Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .kodo/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"model": "x-ai/grok-code-fast-1",
"dir": "/Users/jsimck/Projects/kodo-code/.kodo",
"verbose": false
"verbose": true
}
1,116 changes: 972 additions & 144 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default [
'react-refresh/only-export-components': 'off',
'unicorn/prefer-top-level-await': 'off',
'react/no-multi-comp': 'off',
'react/no-unknown-property': 'off',
},
},
];
62 changes: 33 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,56 +15,60 @@
"author": "Jan Šimeček",
"license": "MIT",
"devDependencies": {
"@effect/language-service": "^0.34.0",
"@effect/language-service": "^0.40.0",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.13.5",
"@types/bun": "^1.2.20",
"@types/bun": "^1.2.22",
"@types/marked-terminal": "^6.1.1",
"@types/node": "^24.3.0",
"@types/react": "^19.1.11",
"@types/node": "^24.5.1",
"@types/react": "^19.1.13",
"@types/turndown": "^5.0.5",
"@utima/eslint-config": "^0.19.0",
"dotenv": "^17.2.1",
"dotenv": "^17.2.2",
"eslint": "8",
"type-fest": "^4.41.0",
"type-fest": "^5.0.0",
"typescript": "^5.9.2"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.20",
"@effect-atom/atom-react": "^0.1.10",
"@effect/platform": "^0.90.6",
"@ai-sdk/openai": "^2.0.30",
"@effect-atom/atom-react": "^0.1.17",
"@effect/platform": "^0.90.9",
"@effect/platform-bun": "^0.79.0",
"@langfuse/client": "^4.0.0-beta.7",
"@langfuse/core": "^4.0.0-beta.7",
"@langfuse/otel": "^4.0.0-beta.7",
"@langfuse/tracing": "^4.0.0-beta.7",
"@langfuse/client": "^4.0.1",
"@langfuse/core": "^4.0.1",
"@langfuse/otel": "^4.0.1",
"@langfuse/tracing": "^4.0.1",
"@mishieck/ink-titled-box": "^0.3.0",
"@openrouter/ai-sdk-provider": "^1.1.2",
"@opentelemetry/auto-instrumentations-node": "^0.62.1",
"@opentelemetry/context-async-hooks": "^2.0.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
"@opentelemetry/sdk-node": "^0.203.0",
"@opentelemetry/sdk-trace-base": "^2.0.1",
"@tanstack/react-query": "^5.85.5",
"ai": "^5.0.23",
"dedent": "^1.6.0",
"effect": "^3.17.9",
"ink": "^6.2.2",
"@openrouter/ai-sdk-provider": "^1.2.0",
"@opentelemetry/auto-instrumentations-node": "^0.64.1",
"@opentelemetry/context-async-hooks": "^2.1.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
"@opentelemetry/sdk-node": "^0.205.0",
"@opentelemetry/sdk-trace-base": "^2.1.0",
"@opentui/core": "snapshot",
"@opentui/react": "snapshot",
"@tanstack/react-query": "^5.89.0",
"ai": "^5.0.44",
"cli-spinners": "^3.2.0",
"dedent": "^1.7.0",
"effect": "^3.17.13",
"ink": "^6.3.0",
"ink-big-text": "^2.0.0",
"ink-gradient": "^3.0.0",
"ink-link": "^4.1.0",
"ink-link": "^5.0.0",
"ink-quicksearch-input": "^1.0.0",
"ink-spawn": "^0.1.4",
"ink-spinner": "^5.0.0",
"ink-syntax-highlight": "^2.0.2",
"ink-task-list": "^2.0.0",
"ink-text-input": "^6.0.0",
"jotai": "^2.13.1",
"langfuse-vercel": "^3.38.4",
"langsmith": "^0.3.63",
"marked": "^16.2.0",
"jotai": "^2.14.0",
"langfuse-vercel": "^3.38.5",
"langsmith": "^0.3.68",
"marked": "^16.3.0",
"marked-terminal": "^7.3.0",
"react": "^19.1.1",
"streamdown": "^1.3.0",
"turndown": "^7.2.1",
"zod": "4"
}
Expand Down
6 changes: 3 additions & 3 deletions src/contexts/session-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EventBus } from '../events/event-bus.ts';
import type { ModelUsage } from '../types.ts';

// import conversationMock from '../../_conversation.json' with { type: 'json' };
// import conversationReasoningMock from '../../conversation_reasoning.json' with { type: 'json' };
import conversationReasoningMock from '../../conversation_reasoning.json' with { type: 'json' };
// import conversationTodosMock from '../../conversation_todos.json' with { type: 'json' };

export type SessionStatus = 'idle' | 'processing';
Expand All @@ -34,9 +34,9 @@ export class SessionContext extends Effect.Service<SessionContext>()(
const eventBus = yield* EventBus;
const state = yield* SubscriptionRef.make<SessionContextState>({
sessionId: randomUUIDv7(),
messages: [],
// messages: [],
// messages: conversationMock as UIMessage[],
// messages: conversationReasoningMock as UIMessage[],
messages: conversationReasoningMock as UIMessage[],
// messages: conversationTodosMock as UIMessage[],
status: 'idle',
cwd: process.cwd(),
Expand Down
19 changes: 13 additions & 6 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { BunRuntime } from '@effect/platform-bun';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { render } from '@opentui/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { Effect } from 'effect';
import { render } from 'ink';
import { LangfuseExporter } from 'langfuse-vercel';

import { AppRuntime } from './layers.ts';
import { Router } from './ui/components/router/router.tsx';
import { RuntimeProvider } from './ui/contexts/runtime-provider.tsx';
import { queryClient } from './ui/lib/query-client.ts';
import { theme } from './ui/themes.ts';

// TODO make open telemetry with custom shutdown logic!
let sdk: NodeSDK | null = null;
Expand All @@ -25,11 +26,17 @@ const app = Effect.scoped(

// Render the application
render(
<RuntimeProvider>
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
</RuntimeProvider>,
<box
style={{ backgroundColor: theme.background }}
height='100%'
width='100%'
>
<RuntimeProvider>
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
</RuntimeProvider>
</box>,
);

// Keep Effect context alive indefinitely
Expand Down
51 changes: 30 additions & 21 deletions src/ui/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { Box, Text } from 'ink';
import Gradient from 'ink-gradient';
import { useAtomValue } from 'jotai';

import { cwdAtom } from '../../state/runtime-atoms.ts';
import { theme } from '../../themes.ts';

export function Header() {
const cwd = useAtomValue(cwdAtom);

return (
<Box justifyContent='center' marginTop={1}>
<Box
<box justifyContent='center'>
<box
borderStyle='double'
borderColor='gray'
flexDirection='column'
paddingY={1}
paddingX={3}
gap={1}
paddingLeft={3}
paddingRight={3}
paddingTop={1}
paddingBottom={1}
flexDirection='column'
alignItems='center'
justifyContent='center'
style={{
borderColor: theme.border,
}}
>
<Box flexDirection='column' alignItems='center' justifyContent='center'>
<Text bold underline>
<Gradient name='atlas'>Kodo Code</Gradient>
</Text>
</Box>
<Text dimColor italic>
An educational take on CLI Coding agents
</Text>
<Text>
<Text dimColor>cwd:</Text> {cwd}
</Text>
</Box>
</Box>
<box flexDirection='column' alignItems='center' justifyContent='center'>
<text fg={theme.primary}>
<u>
<strong>Kodo Code</strong>
</u>
</text>
</box>
<text>
<i fg={theme.mutedForeground}>
An educational take on CLI Coding agents
</i>
</text>
<text>
<span fg={theme.mutedForeground}>cwd:</span> {cwd}
</text>
</box>
</box>
);
}
22 changes: 10 additions & 12 deletions src/ui/components/message/assistant-message.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type { ToolUIPart, UIMessage } from 'ai';
import { Box } from 'ink';
import type { UIMessage } from 'ai';

import { AssistantReasoningPart } from './components/assistant-reasoning-part.tsx';
import { AssistantTextPart } from './components/assistant-text-part.tsx';
import { ToolMessage } from './components/tool-part.tsx';
import type { ToolSetUITools } from '../../../ai/tools/index.ts';

export type AssistantMessageProps = {
Expand All @@ -15,7 +13,7 @@ export type AssistantMessageProps = {
*/
export function AssistantMessage({ message }: AssistantMessageProps) {
return (
<Box flexDirection='column' gap={1} margin={0} padding={0} width='100%'>
<box flexDirection='column' gap={1} margin={0} padding={0} width='100%'>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <AssistantTextPart key={index} part={part} />;
Expand All @@ -25,13 +23,13 @@ export function AssistantMessage({ message }: AssistantMessageProps) {
return <AssistantReasoningPart key={index} part={part} />;
}

if (part.type.startsWith('tool-')) {
return (
<Box key={index}>
<ToolMessage tool={part as ToolUIPart} />
</Box>
);
}
// if (part.type.startsWith('tool-')) {
// return (
// <Box key={index}>
// <ToolMessage tool={part as ToolUIPart} />
// </Box>
// );
// }

// switch (part.type) {
// case 'text':
Expand All @@ -52,6 +50,6 @@ ${JSON.stringify(message.content.object, null, 2)}
\`\`\``}
</Markdown>
)} */}
</Box>
</box>
);
}
77 changes: 37 additions & 40 deletions src/ui/components/message/components/assistant-reasoning-part.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { ReasoningUIPart } from 'ai';
import { Box, Text } from 'ink';
import { useAtomValue } from 'jotai/react';

import { verboseAtom } from '../../../state/runtime-atoms.ts';
import { ShiningText } from '../../shining-text/shining-text.tsx';
import { Markdown } from '../markdown.tsx';
import { theme } from '../../../themes.ts';

export interface AssistantReasoningPartProps {
part: ReasoningUIPart;
Expand All @@ -15,62 +13,61 @@ export function AssistantReasoningPart({ part }: AssistantReasoningPartProps) {
const isLoading = part.state !== 'done';

return (
<Box
borderLeft
borderTop={false}
borderBottom={false}
borderRight={false}
<box
border={['left']}
borderStyle='single'
borderLeftColor='gray'
paddingX={2}
paddingY={verbose ? 1 : 0}
borderColor={theme.border}
paddingLeft={2}
paddingRight={2}
paddingTop={verbose ? 1 : 0}
paddingBottom={verbose ? 1 : 0}
flexDirection='column'
overflow='hidden'
>
{verbose ? (
<>
<Box flexDirection='row' gap={1} marginBottom={1} overflow='hidden'>
<Text color='blue'>
<ShiningText disabled={!isLoading}>
{isLoading ? 'Reasoning...' : 'Reasoning'}
</ShiningText>
</Text>
</Box>
<Box>
<Text dimColor>
<Markdown>{part.text}</Markdown>
</Text>
</Box>
<box flexDirection='row' gap={1} marginBottom={1} overflow='hidden'>
<text fg={theme.primary}>
{/* <ShiningText disabled={!isLoading}> */}
{isLoading ? 'Reasoning...' : 'Reasoning'}
{/* </ShiningText> */}
</text>
</box>
<box>
<text fg={theme.mutedForeground}>
{/* <Markdown>{part.text}</Markdown> */}
{part.text}
</text>
</box>
</>
) : (
<Box
<box
flexDirection='row'
gap={1}
height={1}
justifyContent='space-between'
>
<Box flexDirection='row' gap={1} flexShrink={0} overflow='hidden'>
<Text color='blue'>
<ShiningText disabled={!isLoading}>
<box flexDirection='row' gap={1} flexShrink={0} overflow='hidden'>
<text fg='blue'>
{/* <ShiningText disabled={!isLoading}>
{isLoading ? 'Reasoning...' : 'Reasoning'}
</ShiningText>
</Text>
</Box>
<Box
</ShiningText> */}
</text>
</box>
<box
flexDirection='row'
flexShrink={1}
flexGrow={1}
alignSelf='flex-start'
>
<Text dimColor wrap='truncate-end'>
{part.text}
</Text>
</Box>
<Box flexDirection='row' flexShrink={0}>
<Text>(ctrl+r to expand)</Text>
</Box>
</Box>
{/* wrap='truncate-end' */}
<text fg={theme.mutedForeground}>{part.text}</text>
</box>
<box flexDirection='row' flexShrink={0}>
<text>(ctrl+r to expand)</text>
</box>
</box>
)}
</Box>
</box>
);
}
Loading