@yeshwanthyk/coding-agent 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +372 -0
- package/package.json +42 -0
- package/src/adapters/acp/index.ts +305 -0
- package/src/adapters/acp/protocol.ts +191 -0
- package/src/adapters/acp/session.ts +289 -0
- package/src/adapters/acp/updates.ts +96 -0
- package/src/adapters/cli/headless.ts +112 -0
- package/src/adapters/cli/validate.ts +50 -0
- package/src/adapters/tui/app.tsx +39 -0
- package/src/agent-events.ts +671 -0
- package/src/args.ts +102 -0
- package/src/autocomplete-commands.ts +102 -0
- package/src/commands.ts +23 -0
- package/src/compact-handler.ts +272 -0
- package/src/components/Footer.tsx +49 -0
- package/src/components/Header.tsx +218 -0
- package/src/components/MessageList.tsx +380 -0
- package/src/config.ts +1 -0
- package/src/domain/commands/builtin/clear.ts +14 -0
- package/src/domain/commands/builtin/compact.ts +96 -0
- package/src/domain/commands/builtin/conceal.ts +9 -0
- package/src/domain/commands/builtin/diffwrap.ts +9 -0
- package/src/domain/commands/builtin/editor.ts +24 -0
- package/src/domain/commands/builtin/exit.ts +14 -0
- package/src/domain/commands/builtin/followup.ts +24 -0
- package/src/domain/commands/builtin/index.ts +29 -0
- package/src/domain/commands/builtin/login.ts +118 -0
- package/src/domain/commands/builtin/model.ts +66 -0
- package/src/domain/commands/builtin/status.ts +32 -0
- package/src/domain/commands/builtin/steer.ts +24 -0
- package/src/domain/commands/builtin/theme.ts +23 -0
- package/src/domain/commands/builtin/thinking.ts +16 -0
- package/src/domain/commands/helpers.ts +41 -0
- package/src/domain/commands/registry.ts +42 -0
- package/src/domain/commands/types.ts +69 -0
- package/src/domain/messaging/content.ts +117 -0
- package/src/editor.ts +103 -0
- package/src/extensibility/schema.ts +1 -0
- package/src/extensibility/validation.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAgentEvents.ts +28 -0
- package/src/hooks/useEditorBridge.ts +101 -0
- package/src/hooks/useGitStatus.ts +28 -0
- package/src/hooks/usePromptQueue.ts +7 -0
- package/src/hooks/useSessionController.ts +5 -0
- package/src/hooks/useSpinner.ts +28 -0
- package/src/hooks/useToastManager.ts +26 -0
- package/src/index.ts +188 -0
- package/src/keyboard-handler.ts +134 -0
- package/src/profiler.ts +40 -0
- package/src/runtime/context.tsx +16 -0
- package/src/runtime/factory.ts +63 -0
- package/src/runtime/git/git-info.ts +25 -0
- package/src/runtime/session/session-controller.ts +208 -0
- package/src/session-manager.ts +1 -0
- package/src/session-picker.tsx +134 -0
- package/src/shell-runner.ts +134 -0
- package/src/syntax-highlighting.ts +114 -0
- package/src/theme-names.ts +37 -0
- package/src/tool-ui-contracts.ts +77 -0
- package/src/tui-open-rendering.tsx +565 -0
- package/src/types.ts +89 -0
- package/src/ui/app-shell/TuiApp.tsx +586 -0
- package/src/ui/clipboard/osc52.ts +18 -0
- package/src/ui/components/modals/ConfirmModal.tsx +52 -0
- package/src/ui/components/modals/EditorModal.tsx +39 -0
- package/src/ui/components/modals/InputModal.tsx +30 -0
- package/src/ui/components/modals/ModalContainer.tsx +67 -0
- package/src/ui/components/modals/SelectModal.tsx +48 -0
- package/src/ui/components/modals/index.ts +4 -0
- package/src/ui/features/composer/Composer.tsx +73 -0
- package/src/ui/features/composer/SlashCommandHandler.ts +58 -0
- package/src/ui/features/composer/keyboard.ts +3 -0
- package/src/ui/features/main-view/MainView.tsx +367 -0
- package/src/ui/features/message-pane/MessagePane.tsx +34 -0
- package/src/ui/hooks/useModals.ts +74 -0
- package/src/ui/state/app-store.ts +67 -0
- package/src/utils.ts +14 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header - Single row, minimal by default, click to expand.
|
|
3
|
+
* Left: activity + model·thinking + progress bar + queue
|
|
4
|
+
* Right (expanded): branch + LSP
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Show, createMemo, createSignal } from "solid-js"
|
|
8
|
+
import { useTheme } from "@yeshwanthyk/open-tui"
|
|
9
|
+
import type { ThinkingLevel } from "@yeshwanthyk/agent-core"
|
|
10
|
+
import type { LspManager, LspServerId } from "@yeshwanthyk/lsp"
|
|
11
|
+
import type { ActivityState } from "../types.js"
|
|
12
|
+
|
|
13
|
+
const LSP_SYMBOLS: Record<LspServerId, [string, string]> = {
|
|
14
|
+
typescript: ["⬡", "⬢"],
|
|
15
|
+
biome: ["✧", "✦"],
|
|
16
|
+
basedpyright: ["ψ", "Ψ"],
|
|
17
|
+
ruff: ["△", "▲"],
|
|
18
|
+
ty: ["τ", "Τ"],
|
|
19
|
+
gopls: ["◎", "◉"],
|
|
20
|
+
"rust-analyzer": ["⛭", "⚙"],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Robot face icons for each activity state */
|
|
24
|
+
const ACTIVITY_FACES: Record<ActivityState, { face: string; label: string }> = {
|
|
25
|
+
idle: { face: "● ‿ ●", label: "ready" },
|
|
26
|
+
thinking: { face: "● ≋ ●", label: "think" },
|
|
27
|
+
streaming: { face: "● ◦ ●", label: "stream" },
|
|
28
|
+
tool: { face: "● ⏅ ●", label: "run" },
|
|
29
|
+
compacting: { face: "● ≡ ●", label: "pack" },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Animated faces for each active state */
|
|
33
|
+
const ANIMATED_FACES: Partial<Record<ActivityState, string[]>> = {
|
|
34
|
+
streaming: ["● ◦ ●", "● ○ ●", "● ◦ ●", "● ∘ ●"], // mouth moves (talking)
|
|
35
|
+
thinking: ["● ≋ ●", "● ~ ●", "● ≈ ●", "● ~ ●"], // squiggly (pondering)
|
|
36
|
+
tool: ["● ⏅ ●", "● ⏆ ●", "● ⏅ ●", "● ⏆ ●"], // steps running
|
|
37
|
+
compacting: ["● ≡ ●", "● = ●", "● - ●", "● = ●"], // compress animation
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fixed width for activity section to prevent layout shift */
|
|
41
|
+
const ACTIVITY_WIDTH = 13
|
|
42
|
+
|
|
43
|
+
/** Progress bar characters */
|
|
44
|
+
const PROGRESS_FILLED = "━"
|
|
45
|
+
const PROGRESS_EMPTY = "┄"
|
|
46
|
+
const PROGRESS_BAR_LENGTH = 8
|
|
47
|
+
|
|
48
|
+
import type { QueueCounts } from "@yeshwanthyk/runtime-effect/session/prompt-queue.js"
|
|
49
|
+
|
|
50
|
+
export interface HeaderProps {
|
|
51
|
+
modelId: string
|
|
52
|
+
thinking: ThinkingLevel
|
|
53
|
+
contextTokens: number
|
|
54
|
+
contextWindow: number
|
|
55
|
+
queueCounts: QueueCounts
|
|
56
|
+
activityState: ActivityState
|
|
57
|
+
retryStatus: string | null
|
|
58
|
+
lspActive: boolean
|
|
59
|
+
spinnerFrame: number
|
|
60
|
+
lsp: LspManager
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Header(props: HeaderProps) {
|
|
64
|
+
const { theme } = useTheme()
|
|
65
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
66
|
+
|
|
67
|
+
// Model·thinking combined
|
|
68
|
+
const modelThinking = createMemo(() => {
|
|
69
|
+
const model = props.modelId.replace(/^claude-/, "").replace(/-latest$/, "")
|
|
70
|
+
if (props.thinking === "off") return model
|
|
71
|
+
// Abbreviate thinking level
|
|
72
|
+
const thinkingAbbrev: Record<ThinkingLevel, string> = {
|
|
73
|
+
off: "",
|
|
74
|
+
minimal: "min",
|
|
75
|
+
low: "low",
|
|
76
|
+
medium: "med",
|
|
77
|
+
high: "high",
|
|
78
|
+
xhigh: "xhi",
|
|
79
|
+
}
|
|
80
|
+
return `${model}·${thinkingAbbrev[props.thinking]}`
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Activity with robot face
|
|
84
|
+
const activity = createMemo(() => {
|
|
85
|
+
if (props.retryStatus) {
|
|
86
|
+
return { face: "● ! ●", label: "retry", color: theme.warning }
|
|
87
|
+
}
|
|
88
|
+
const state = props.activityState
|
|
89
|
+
const base = ACTIVITY_FACES[state]
|
|
90
|
+
|
|
91
|
+
// Animate face for active states
|
|
92
|
+
let face = base.face
|
|
93
|
+
const frames = ANIMATED_FACES[state]
|
|
94
|
+
if (frames) {
|
|
95
|
+
const frameIndex = props.spinnerFrame % frames.length
|
|
96
|
+
face = frames[frameIndex] ?? base.face
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const color = state === "idle" ? theme.textMuted : theme.accent
|
|
100
|
+
return { face, label: base.label, color }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Progress bar with percentage
|
|
104
|
+
const progressBar = createMemo(() => {
|
|
105
|
+
if (props.contextWindow <= 0) return null
|
|
106
|
+
const pct = props.contextTokens > 0
|
|
107
|
+
? Math.min(100, (props.contextTokens / props.contextWindow) * 100)
|
|
108
|
+
: 0
|
|
109
|
+
const filled = Math.round((pct / 100) * PROGRESS_BAR_LENGTH)
|
|
110
|
+
const empty = PROGRESS_BAR_LENGTH - filled
|
|
111
|
+
const bar = PROGRESS_FILLED.repeat(filled) + PROGRESS_EMPTY.repeat(empty)
|
|
112
|
+
const color = pct > 90 ? theme.error : pct > 70 ? theme.warning : pct > 40 ? theme.text : theme.success
|
|
113
|
+
return { bar, pct: Math.round(pct), color }
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Queue indicator
|
|
117
|
+
const queueIndicator = createMemo(() => {
|
|
118
|
+
const { steer, followUp } = props.queueCounts
|
|
119
|
+
if (steer <= 0 && followUp <= 0) return null
|
|
120
|
+
const parts: string[] = []
|
|
121
|
+
if (steer > 0) {
|
|
122
|
+
parts.push(`⚡${steer}`)
|
|
123
|
+
}
|
|
124
|
+
if (followUp > 0) {
|
|
125
|
+
parts.push(`…${followUp}`)
|
|
126
|
+
}
|
|
127
|
+
return parts.join(" ")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
// LSP status
|
|
132
|
+
const lspStatus = createMemo(() => {
|
|
133
|
+
const servers = props.lsp.activeServers()
|
|
134
|
+
if (servers.length === 0) return null
|
|
135
|
+
const uniqueIds = [...new Set(servers.map((s) => s.serverId))]
|
|
136
|
+
const symbolIndex = props.lspActive ? 1 : 0
|
|
137
|
+
const symbols = uniqueIds.map((id) => LSP_SYMBOLS[id]?.[symbolIndex] ?? id).join("")
|
|
138
|
+
const counts = props.lsp.diagnosticCounts()
|
|
139
|
+
return { symbols, errors: counts.errors, warnings: counts.warnings }
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const toggleExpanded = () => setExpanded((v) => !v)
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<box
|
|
146
|
+
flexDirection="row"
|
|
147
|
+
flexShrink={0}
|
|
148
|
+
paddingLeft={1}
|
|
149
|
+
paddingRight={1}
|
|
150
|
+
border={["top", "bottom", "left", "right"]}
|
|
151
|
+
borderStyle="rounded"
|
|
152
|
+
borderColor={theme.border}
|
|
153
|
+
onMouseUp={(e: { isSelecting?: boolean }) => {
|
|
154
|
+
if (e.isSelecting) return
|
|
155
|
+
toggleExpanded()
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{/* Left section: Activity + Model·Thinking + Progress + Queue */}
|
|
159
|
+
<box flexDirection="row" flexShrink={0} gap={1}>
|
|
160
|
+
{/* Activity (fixed width) */}
|
|
161
|
+
<box minWidth={ACTIVITY_WIDTH}>
|
|
162
|
+
<text>
|
|
163
|
+
<span style={{ fg: activity().color }}>{activity().face}</span>
|
|
164
|
+
<span style={{ fg: theme.textMuted }}> {activity().label}</span>
|
|
165
|
+
</text>
|
|
166
|
+
</box>
|
|
167
|
+
|
|
168
|
+
{/* Model·Thinking */}
|
|
169
|
+
<text fg={theme.text}>{modelThinking()}</text>
|
|
170
|
+
|
|
171
|
+
{/* Progress bar */}
|
|
172
|
+
<Show when={progressBar()} keyed>
|
|
173
|
+
{(prog) => (
|
|
174
|
+
<text>
|
|
175
|
+
<span style={{ fg: prog.color }}>{prog.bar}</span>
|
|
176
|
+
<span style={{ fg: theme.textMuted }}> {prog.pct}%</span>
|
|
177
|
+
</text>
|
|
178
|
+
)}
|
|
179
|
+
</Show>
|
|
180
|
+
|
|
181
|
+
{/* Queue */}
|
|
182
|
+
<Show when={queueIndicator()}>
|
|
183
|
+
<text fg={theme.warning}>{queueIndicator()}</text>
|
|
184
|
+
</Show>
|
|
185
|
+
</box>
|
|
186
|
+
|
|
187
|
+
{/* Spacer */}
|
|
188
|
+
<box flexGrow={1} />
|
|
189
|
+
|
|
190
|
+
{/* Right section (only when expanded): Branch + LSP */}
|
|
191
|
+
<Show when={expanded()}>
|
|
192
|
+
<box flexDirection="row" flexShrink={0} gap={1}>
|
|
193
|
+
|
|
194
|
+
{/* LSP */}
|
|
195
|
+
<Show when={lspStatus()} keyed>
|
|
196
|
+
{(lsp) => (
|
|
197
|
+
<text>
|
|
198
|
+
<span style={{ fg: props.lspActive ? theme.accent : theme.success }}>{lsp.symbols}</span>
|
|
199
|
+
<Show when={lsp.errors > 0 || lsp.warnings > 0}>
|
|
200
|
+
<span style={{ fg: theme.textMuted }}> </span>
|
|
201
|
+
<Show when={lsp.errors > 0}>
|
|
202
|
+
<span style={{ fg: theme.error }}>{lsp.errors}</span>
|
|
203
|
+
</Show>
|
|
204
|
+
<Show when={lsp.errors > 0 && lsp.warnings > 0}>
|
|
205
|
+
<span style={{ fg: theme.textMuted }}>/</span>
|
|
206
|
+
</Show>
|
|
207
|
+
<Show when={lsp.warnings > 0}>
|
|
208
|
+
<span style={{ fg: theme.warning }}>{lsp.warnings}</span>
|
|
209
|
+
</Show>
|
|
210
|
+
</Show>
|
|
211
|
+
</text>
|
|
212
|
+
)}
|
|
213
|
+
</Show>
|
|
214
|
+
</box>
|
|
215
|
+
</Show>
|
|
216
|
+
</box>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageList component for rendering conversation content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { For, Show, Switch, Match, createMemo } from "solid-js"
|
|
6
|
+
import { Markdown, useTheme } from "@yeshwanthyk/open-tui"
|
|
7
|
+
import type { UIMessage, ToolBlock, ContentItem } from "../types.js"
|
|
8
|
+
import { profile } from "../profiler.js"
|
|
9
|
+
import { ToolBlock as ToolBlockComponent } from "../tui-open-rendering.js"
|
|
10
|
+
|
|
11
|
+
// ----- Tool Block Wrapper -----
|
|
12
|
+
|
|
13
|
+
function ToolBlockWrapper(props: {
|
|
14
|
+
tool: ToolBlock
|
|
15
|
+
isExpanded: (id: string) => boolean
|
|
16
|
+
onToggle: (id: string) => void
|
|
17
|
+
diffWrapMode: "word" | "none"
|
|
18
|
+
onEditFile?: (path: string, line?: number) => void
|
|
19
|
+
}) {
|
|
20
|
+
const expanded = createMemo(() => props.isExpanded(props.tool.id))
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ToolBlockComponent
|
|
24
|
+
name={props.tool.name}
|
|
25
|
+
args={props.tool.args}
|
|
26
|
+
output={props.tool.output || null}
|
|
27
|
+
editDiff={props.tool.editDiff || null}
|
|
28
|
+
isError={props.tool.isError}
|
|
29
|
+
isComplete={props.tool.isComplete}
|
|
30
|
+
expanded={expanded()}
|
|
31
|
+
diffWrapMode={props.diffWrapMode}
|
|
32
|
+
onToggleExpanded={() => props.onToggle(props.tool.id)}
|
|
33
|
+
onEditFile={props.onEditFile}
|
|
34
|
+
// Custom tool metadata for first-class rendering
|
|
35
|
+
label={props.tool.label}
|
|
36
|
+
source={props.tool.source}
|
|
37
|
+
sourcePath={props.tool.sourcePath}
|
|
38
|
+
result={props.tool.result}
|
|
39
|
+
renderCall={props.tool.renderCall}
|
|
40
|
+
renderResult={props.tool.renderResult}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ----- Thinking Block Wrapper -----
|
|
46
|
+
|
|
47
|
+
const THINKING_MAX_WIDTH = 50
|
|
48
|
+
|
|
49
|
+
function truncateThinking(text: string): string {
|
|
50
|
+
const firstLine = text.split("\n")[0] || ""
|
|
51
|
+
if (firstLine.length <= THINKING_MAX_WIDTH) return firstLine
|
|
52
|
+
return firstLine.slice(0, THINKING_MAX_WIDTH - 3) + "..."
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ThinkingBlockWrapper(props: {
|
|
56
|
+
id: string
|
|
57
|
+
summary: string
|
|
58
|
+
preview?: string
|
|
59
|
+
full: string
|
|
60
|
+
isExpanded: (id: string) => boolean
|
|
61
|
+
onToggle: (id: string) => void
|
|
62
|
+
concealMarkdown?: boolean
|
|
63
|
+
}) {
|
|
64
|
+
const { theme } = useTheme()
|
|
65
|
+
const expanded = createMemo(() => props.isExpanded(props.id))
|
|
66
|
+
const preview = () => props.preview || truncateThinking(props.summary || props.full)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<box paddingLeft={4} flexDirection="column">
|
|
70
|
+
<box
|
|
71
|
+
flexDirection="row"
|
|
72
|
+
onMouseUp={(e: { isSelecting?: boolean }) => {
|
|
73
|
+
if (e.isSelecting) return
|
|
74
|
+
props.onToggle(props.id)
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<text selectable={false} fg={theme.textMuted}>
|
|
78
|
+
{expanded() ? "▾" : "▸"} {preview()}
|
|
79
|
+
</text>
|
|
80
|
+
</box>
|
|
81
|
+
<Show when={expanded()}>
|
|
82
|
+
<box paddingLeft={2} paddingTop={1}>
|
|
83
|
+
<Markdown text={props.full} conceal={props.concealMarkdown} dim />
|
|
84
|
+
</box>
|
|
85
|
+
</Show>
|
|
86
|
+
</box>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ----- Content Items Builder -----
|
|
91
|
+
|
|
92
|
+
// Per-item cache: reuse ContentItem objects when data unchanged
|
|
93
|
+
// Key format: "type:id" or "type:msgId:blockIdx"
|
|
94
|
+
const itemCache = new Map<string, ContentItem>()
|
|
95
|
+
let lastMessageCount = 0
|
|
96
|
+
let lastFirstMessageId: string | null = null
|
|
97
|
+
|
|
98
|
+
/** Get or create a cached ContentItem, preserving object identity when data matches */
|
|
99
|
+
function getCachedItem<T extends ContentItem>(
|
|
100
|
+
key: string,
|
|
101
|
+
current: T,
|
|
102
|
+
isEqual: (a: T, b: T) => boolean
|
|
103
|
+
): T {
|
|
104
|
+
const cached = itemCache.get(key) as T | undefined
|
|
105
|
+
if (cached && cached.type === current.type && isEqual(cached, current)) {
|
|
106
|
+
return cached
|
|
107
|
+
}
|
|
108
|
+
itemCache.set(key, current)
|
|
109
|
+
return current
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildContentItems(
|
|
113
|
+
messages: UIMessage[],
|
|
114
|
+
toolBlocks: ToolBlock[],
|
|
115
|
+
thinkingVisible: boolean
|
|
116
|
+
): ContentItem[] {
|
|
117
|
+
// Prune stale cache entries when message count decreases (e.g., clear)
|
|
118
|
+
if (messages.length < lastMessageCount) {
|
|
119
|
+
itemCache.clear()
|
|
120
|
+
}
|
|
121
|
+
lastMessageCount = messages.length
|
|
122
|
+
const firstMessageId = messages.length > 0 ? messages[0].id : null
|
|
123
|
+
if (firstMessageId !== lastFirstMessageId) {
|
|
124
|
+
if (lastFirstMessageId !== null) itemCache.clear()
|
|
125
|
+
lastFirstMessageId = firstMessageId
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const items: ContentItem[] = []
|
|
129
|
+
const renderedToolIds = new Set<string>()
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < messages.length; i++) {
|
|
132
|
+
const msg = messages[i]
|
|
133
|
+
const isLastMessage = i === messages.length - 1
|
|
134
|
+
|
|
135
|
+
if (msg.role === "user") {
|
|
136
|
+
const item: ContentItem = { type: "user", content: msg.content }
|
|
137
|
+
items.push(
|
|
138
|
+
getCachedItem(`user:${msg.id}`, item, (a, b) => a.content === b.content)
|
|
139
|
+
)
|
|
140
|
+
} else if (msg.role === "assistant") {
|
|
141
|
+
// Use contentBlocks if available (preserves interleaved order)
|
|
142
|
+
if (msg.contentBlocks && msg.contentBlocks.length > 0) {
|
|
143
|
+
for (let blockIdx = 0; blockIdx < msg.contentBlocks.length; blockIdx++) {
|
|
144
|
+
const block = msg.contentBlocks[blockIdx]
|
|
145
|
+
if (block.type === "thinking") {
|
|
146
|
+
if (thinkingVisible) {
|
|
147
|
+
const item: ContentItem = {
|
|
148
|
+
type: "thinking",
|
|
149
|
+
id: block.id,
|
|
150
|
+
summary: block.summary,
|
|
151
|
+
preview: block.preview,
|
|
152
|
+
full: block.full,
|
|
153
|
+
isStreaming: msg.isStreaming,
|
|
154
|
+
}
|
|
155
|
+
items.push(
|
|
156
|
+
getCachedItem(`thinking:${msg.id}:${block.id}`, item, (a, b) =>
|
|
157
|
+
a.type === "thinking" && b.type === "thinking" &&
|
|
158
|
+
a.full === b.full && a.isStreaming === b.isStreaming
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
} else if (block.type === "text") {
|
|
163
|
+
if (block.text) {
|
|
164
|
+
const item: ContentItem = { type: "assistant", content: block.text, isStreaming: msg.isStreaming }
|
|
165
|
+
if (msg.isStreaming) {
|
|
166
|
+
items.push(item)
|
|
167
|
+
} else {
|
|
168
|
+
items.push(
|
|
169
|
+
getCachedItem(`text:${msg.id}:${blockIdx}:final`, item, (a, b) =>
|
|
170
|
+
a.type === "assistant" && b.type === "assistant" &&
|
|
171
|
+
a.content === b.content && a.isStreaming === b.isStreaming
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (block.type === "tool") {
|
|
177
|
+
if (!renderedToolIds.has(block.tool.id)) {
|
|
178
|
+
const item: ContentItem = { type: "tool", tool: block.tool }
|
|
179
|
+
items.push(
|
|
180
|
+
getCachedItem(`tool:${block.tool.id}:${block.tool.isComplete}`, item, (a, b) =>
|
|
181
|
+
a.type === "tool" && b.type === "tool" &&
|
|
182
|
+
a.tool.id === b.tool.id && a.tool.isComplete === b.tool.isComplete &&
|
|
183
|
+
a.tool.output === b.tool.output &&
|
|
184
|
+
(a.tool.updateSeq ?? 0) === (b.tool.updateSeq ?? 0)
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
renderedToolIds.add(block.tool.id)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Fallback: legacy format without contentBlocks
|
|
193
|
+
if (thinkingVisible && msg.thinking) {
|
|
194
|
+
const item: ContentItem = {
|
|
195
|
+
type: "thinking",
|
|
196
|
+
id: `thinking-${msg.id}`,
|
|
197
|
+
summary: msg.thinking.summary,
|
|
198
|
+
preview: msg.thinking.preview || truncateThinking(msg.thinking.summary || msg.thinking.full),
|
|
199
|
+
full: msg.thinking.full,
|
|
200
|
+
isStreaming: msg.isStreaming,
|
|
201
|
+
}
|
|
202
|
+
items.push(
|
|
203
|
+
getCachedItem(`thinking:${msg.id}`, item, (a, b) =>
|
|
204
|
+
a.type === "thinking" && b.type === "thinking" &&
|
|
205
|
+
a.full === b.full && a.isStreaming === b.isStreaming
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const tool of msg.tools || []) {
|
|
211
|
+
if (!renderedToolIds.has(tool.id)) {
|
|
212
|
+
const item: ContentItem = { type: "tool", tool }
|
|
213
|
+
items.push(
|
|
214
|
+
getCachedItem(`tool:${tool.id}:${tool.isComplete}`, item, (a, b) =>
|
|
215
|
+
a.type === "tool" && b.type === "tool" &&
|
|
216
|
+
a.tool.id === b.tool.id && a.tool.isComplete === b.tool.isComplete &&
|
|
217
|
+
a.tool.output === b.tool.output &&
|
|
218
|
+
(a.tool.updateSeq ?? 0) === (b.tool.updateSeq ?? 0)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
renderedToolIds.add(tool.id)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (msg.content) {
|
|
226
|
+
const item: ContentItem = { type: "assistant", content: msg.content, isStreaming: msg.isStreaming }
|
|
227
|
+
if (msg.isStreaming) {
|
|
228
|
+
items.push(item)
|
|
229
|
+
} else {
|
|
230
|
+
items.push(
|
|
231
|
+
getCachedItem(`text:${msg.id}:final`, item, (a, b) =>
|
|
232
|
+
a.type === "assistant" && b.type === "assistant" &&
|
|
233
|
+
a.content === b.content && a.isStreaming === b.isStreaming
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// For last message, include orphan toolBlocks from global state
|
|
241
|
+
if (isLastMessage) {
|
|
242
|
+
for (const tool of toolBlocks) {
|
|
243
|
+
if (!renderedToolIds.has(tool.id)) {
|
|
244
|
+
const item: ContentItem = { type: "tool", tool }
|
|
245
|
+
items.push(
|
|
246
|
+
getCachedItem(`tool:${tool.id}:${tool.isComplete}`, item, (a, b) =>
|
|
247
|
+
a.type === "tool" && b.type === "tool" &&
|
|
248
|
+
a.tool.id === b.tool.id && a.tool.isComplete === b.tool.isComplete &&
|
|
249
|
+
a.tool.output === b.tool.output &&
|
|
250
|
+
(a.tool.updateSeq ?? 0) === (b.tool.updateSeq ?? 0)
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
renderedToolIds.add(tool.id)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else if (msg.role === "shell") {
|
|
258
|
+
const item: ContentItem = {
|
|
259
|
+
type: "shell",
|
|
260
|
+
command: msg.command,
|
|
261
|
+
output: msg.output,
|
|
262
|
+
exitCode: msg.exitCode,
|
|
263
|
+
truncated: msg.truncated,
|
|
264
|
+
tempFilePath: msg.tempFilePath,
|
|
265
|
+
}
|
|
266
|
+
items.push(
|
|
267
|
+
getCachedItem(`shell:${msg.id}`, item, (a, b) =>
|
|
268
|
+
a.type === "shell" && b.type === "shell" &&
|
|
269
|
+
a.command === b.command && a.output === b.output
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return items
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ----- MessageList Component -----
|
|
279
|
+
|
|
280
|
+
export interface MessageListProps {
|
|
281
|
+
messages: UIMessage[]
|
|
282
|
+
toolBlocks: ToolBlock[]
|
|
283
|
+
thinkingVisible: boolean
|
|
284
|
+
diffWrapMode: "word" | "none"
|
|
285
|
+
concealMarkdown?: boolean
|
|
286
|
+
isToolExpanded: (id: string) => boolean
|
|
287
|
+
toggleToolExpanded: (id: string) => void
|
|
288
|
+
isThinkingExpanded: (id: string) => boolean
|
|
289
|
+
toggleThinkingExpanded: (id: string) => void
|
|
290
|
+
onEditFile?: (path: string, line?: number) => void
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function MessageList(props: MessageListProps) {
|
|
294
|
+
const { theme } = useTheme()
|
|
295
|
+
|
|
296
|
+
const contentItems = createMemo(() =>
|
|
297
|
+
profile("build_content_items", () =>
|
|
298
|
+
buildContentItems(props.messages, props.toolBlocks, props.thinkingVisible)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<box flexDirection="column" gap={1} paddingTop={1}>
|
|
304
|
+
<For each={contentItems()}>
|
|
305
|
+
{(item) => (
|
|
306
|
+
<Switch>
|
|
307
|
+
<Match when={item.type === "user" && item}>
|
|
308
|
+
{(userItem) => (
|
|
309
|
+
<box paddingLeft={1}>
|
|
310
|
+
<text fg={theme.primary}>
|
|
311
|
+
<span>{"§ "}</span>
|
|
312
|
+
{userItem().content}
|
|
313
|
+
</text>
|
|
314
|
+
</box>
|
|
315
|
+
)}
|
|
316
|
+
</Match>
|
|
317
|
+
<Match when={item.type === "thinking" && item}>
|
|
318
|
+
{(thinkingItem) => (
|
|
319
|
+
<ThinkingBlockWrapper
|
|
320
|
+
id={thinkingItem().id}
|
|
321
|
+
summary={thinkingItem().summary}
|
|
322
|
+
preview={thinkingItem().preview}
|
|
323
|
+
full={thinkingItem().full}
|
|
324
|
+
isExpanded={props.isThinkingExpanded}
|
|
325
|
+
onToggle={props.toggleThinkingExpanded}
|
|
326
|
+
concealMarkdown={props.concealMarkdown}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
</Match>
|
|
330
|
+
<Match when={item.type === "assistant" && item}>
|
|
331
|
+
{(assistantItem) => (
|
|
332
|
+
<box paddingLeft={1}>
|
|
333
|
+
<Markdown
|
|
334
|
+
text={assistantItem().content}
|
|
335
|
+
conceal={props.concealMarkdown}
|
|
336
|
+
streaming={assistantItem().isStreaming}
|
|
337
|
+
/>
|
|
338
|
+
</box>
|
|
339
|
+
)}
|
|
340
|
+
</Match>
|
|
341
|
+
<Match when={item.type === "tool" && item}>
|
|
342
|
+
{(toolItem) => (
|
|
343
|
+
<box paddingLeft={6}>
|
|
344
|
+
<ToolBlockWrapper
|
|
345
|
+
tool={toolItem().tool}
|
|
346
|
+
isExpanded={props.isToolExpanded}
|
|
347
|
+
onToggle={props.toggleToolExpanded}
|
|
348
|
+
diffWrapMode={props.diffWrapMode}
|
|
349
|
+
onEditFile={props.onEditFile}
|
|
350
|
+
/>
|
|
351
|
+
</box>
|
|
352
|
+
)}
|
|
353
|
+
</Match>
|
|
354
|
+
<Match when={item.type === "shell" && item}>
|
|
355
|
+
{(shellItem) => (
|
|
356
|
+
<box paddingLeft={1} flexDirection="column">
|
|
357
|
+
<text fg={theme.warning}>
|
|
358
|
+
<span>{"$ "}</span>
|
|
359
|
+
{shellItem().command}
|
|
360
|
+
</text>
|
|
361
|
+
<Show when={shellItem().output}>
|
|
362
|
+
<box paddingLeft={2} paddingTop={1}>
|
|
363
|
+
<text fg={theme.textMuted}>{shellItem().output}</text>
|
|
364
|
+
</box>
|
|
365
|
+
</Show>
|
|
366
|
+
<Show when={shellItem().exitCode !== null && shellItem().exitCode !== 0}>
|
|
367
|
+
<text fg={theme.error}>{`exit ${shellItem().exitCode}`}</text>
|
|
368
|
+
</Show>
|
|
369
|
+
<Show when={shellItem().truncated && shellItem().tempFilePath}>
|
|
370
|
+
<text fg={theme.textMuted}>{`[truncated, full output: ${shellItem().tempFilePath}]`}</text>
|
|
371
|
+
</Show>
|
|
372
|
+
</box>
|
|
373
|
+
)}
|
|
374
|
+
</Match>
|
|
375
|
+
</Switch>
|
|
376
|
+
)}
|
|
377
|
+
</For>
|
|
378
|
+
</box>
|
|
379
|
+
)
|
|
380
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@yeshwanthyk/runtime-effect/config";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CommandDefinition } from "../types.js"
|
|
2
|
+
|
|
3
|
+
export const clearCommand: CommandDefinition = {
|
|
4
|
+
name: "clear",
|
|
5
|
+
execute: (_args, ctx) => {
|
|
6
|
+
ctx.setMessages(() => [])
|
|
7
|
+
ctx.setToolBlocks(() => [])
|
|
8
|
+
ctx.setContextTokens(0)
|
|
9
|
+
ctx.setCacheStats(null)
|
|
10
|
+
ctx.agent.reset()
|
|
11
|
+
void ctx.hookRunner?.emit({ type: "session.clear", sessionId: null })
|
|
12
|
+
return true
|
|
13
|
+
},
|
|
14
|
+
}
|