@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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Session - wraps Agent and emits ACP updates
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Agent, AgentEvent, Attachment } from "@yeshwanthyk/agent-core"
|
|
6
|
+
import type { UpdateEmitter } from "./updates.js"
|
|
7
|
+
import type { ContentBlock, SlashCommand, ModelOption, StopReason } from "./protocol.js"
|
|
8
|
+
import { textChunk, thoughtChunk, toolCall, toolCallUpdate, toolNameToKind } from "./updates.js"
|
|
9
|
+
import type { SessionOrchestratorService } from "@yeshwanthyk/runtime-effect/session/orchestrator.js"
|
|
10
|
+
import { Effect } from "effect"
|
|
11
|
+
|
|
12
|
+
export interface AcpSessionConfig {
|
|
13
|
+
sessionId: string
|
|
14
|
+
cwd: string
|
|
15
|
+
agent: Agent
|
|
16
|
+
sessionOrchestrator: SessionOrchestratorService
|
|
17
|
+
emitter: UpdateEmitter
|
|
18
|
+
models: ModelOption[]
|
|
19
|
+
currentModelId: string
|
|
20
|
+
contextWindow: number
|
|
21
|
+
thinkingLevel: string
|
|
22
|
+
setModel: (modelId: string) => boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AcpSession {
|
|
26
|
+
id: string
|
|
27
|
+
cwd: string
|
|
28
|
+
prompt(content: ContentBlock[]): Promise<StopReason>
|
|
29
|
+
cancel(): void
|
|
30
|
+
getAvailableCommands(): SlashCommand[]
|
|
31
|
+
getModels(): { options: ModelOption[]; currentModelId: string }
|
|
32
|
+
setModel(modelId: string): boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Slash commands exposed to Zed
|
|
36
|
+
const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
37
|
+
{ name: "model", description: "Switch model: /model <modelId>" },
|
|
38
|
+
{ name: "thinking", description: "Set thinking: /thinking off|minimal|low|medium|high|xhigh" },
|
|
39
|
+
{ name: "status", description: "Show session status" },
|
|
40
|
+
{ name: "compact", description: "Compact conversation context" },
|
|
41
|
+
{ name: "clear", description: "Clear conversation" },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export function createAcpSession(config: AcpSessionConfig): AcpSession {
|
|
45
|
+
const { sessionId, cwd, agent, emitter, models, contextWindow, sessionOrchestrator } = config
|
|
46
|
+
let currentModelId = config.currentModelId
|
|
47
|
+
let thinkingLevel = config.thinkingLevel
|
|
48
|
+
let cancelled = false
|
|
49
|
+
let unsubscribe: (() => void) | null = null
|
|
50
|
+
|
|
51
|
+
// Track session stats
|
|
52
|
+
let turnCount = 0
|
|
53
|
+
let lastUsage: { totalTokens: number; cacheRead?: number; cacheWrite?: number } | null = null
|
|
54
|
+
|
|
55
|
+
// Track emitted content to avoid duplicate chunks
|
|
56
|
+
let lastEmittedTextLen = 0
|
|
57
|
+
let lastEmittedThinkingLen = 0
|
|
58
|
+
|
|
59
|
+
// Subscribe to agent events and emit ACP updates
|
|
60
|
+
function subscribeToEvents(): () => void {
|
|
61
|
+
lastEmittedTextLen = 0
|
|
62
|
+
lastEmittedThinkingLen = 0
|
|
63
|
+
|
|
64
|
+
return agent.subscribe((event: AgentEvent) => {
|
|
65
|
+
if (cancelled) return
|
|
66
|
+
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case "message_update":
|
|
69
|
+
if (event.message.role === "assistant") {
|
|
70
|
+
// Extract text and thinking from content
|
|
71
|
+
const content = event.message.content as unknown[]
|
|
72
|
+
let totalText = ""
|
|
73
|
+
let totalThinking = ""
|
|
74
|
+
|
|
75
|
+
for (const block of content) {
|
|
76
|
+
if (typeof block !== "object" || block === null) continue
|
|
77
|
+
const b = block as Record<string, unknown>
|
|
78
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
79
|
+
totalText += b.text
|
|
80
|
+
} else if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
81
|
+
totalThinking += b.thinking
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Emit only new content (delta)
|
|
86
|
+
if (totalText.length > lastEmittedTextLen) {
|
|
87
|
+
const delta = totalText.slice(lastEmittedTextLen)
|
|
88
|
+
lastEmittedTextLen = totalText.length
|
|
89
|
+
emitter.emit(textChunk(delta))
|
|
90
|
+
}
|
|
91
|
+
if (totalThinking.length > lastEmittedThinkingLen) {
|
|
92
|
+
const delta = totalThinking.slice(lastEmittedThinkingLen)
|
|
93
|
+
lastEmittedThinkingLen = totalThinking.length
|
|
94
|
+
emitter.emit(thoughtChunk(delta))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
case "tool_execution_start":
|
|
100
|
+
emitter.emit(
|
|
101
|
+
toolCall(
|
|
102
|
+
event.toolCallId,
|
|
103
|
+
event.toolName,
|
|
104
|
+
toolNameToKind(event.toolName),
|
|
105
|
+
event.args
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
case "tool_execution_update":
|
|
111
|
+
emitter.emit(
|
|
112
|
+
toolCallUpdate(
|
|
113
|
+
event.toolCallId,
|
|
114
|
+
"in_progress",
|
|
115
|
+
event.partialResult ? JSON.stringify(event.partialResult) : undefined
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
case "tool_execution_end":
|
|
121
|
+
emitter.emit(
|
|
122
|
+
toolCallUpdate(
|
|
123
|
+
event.toolCallId,
|
|
124
|
+
event.isError ? "failed" : "completed",
|
|
125
|
+
event.result ? JSON.stringify(event.result) : undefined
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
case "message_end":
|
|
131
|
+
// Capture usage from assistant messages
|
|
132
|
+
if (event.message.role === "assistant") {
|
|
133
|
+
const msg = event.message as { usage?: { totalTokens?: number; cacheRead?: number; cacheWrite?: number } }
|
|
134
|
+
if (msg.usage?.totalTokens) {
|
|
135
|
+
lastUsage = {
|
|
136
|
+
totalTokens: msg.usage.totalTokens,
|
|
137
|
+
cacheRead: msg.usage.cacheRead,
|
|
138
|
+
cacheWrite: msg.usage.cacheWrite,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function prompt(content: ContentBlock[]): Promise<StopReason> {
|
|
148
|
+
cancelled = false
|
|
149
|
+
turnCount++
|
|
150
|
+
unsubscribe = subscribeToEvents()
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Check for slash command
|
|
154
|
+
const firstText = content.find((b) => b.type === "text")?.text?.trim()
|
|
155
|
+
if (firstText?.startsWith("/")) {
|
|
156
|
+
return handleSlashCommand(firstText)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Extract text and images
|
|
160
|
+
let textContent = ""
|
|
161
|
+
const images: Array<{ data: string; mimeType: string }> = []
|
|
162
|
+
|
|
163
|
+
for (const block of content) {
|
|
164
|
+
if (block.type === "text" && block.text) {
|
|
165
|
+
textContent += block.text
|
|
166
|
+
} else if (block.type === "image" && block.data && block.mimeType) {
|
|
167
|
+
images.push({ data: block.data, mimeType: block.mimeType })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!textContent && images.length === 0) {
|
|
172
|
+
return "end_turn"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Convert images to attachment format
|
|
176
|
+
const attachments: Attachment[] = images.map((img, idx) => ({
|
|
177
|
+
id: `acp-img-${idx}`,
|
|
178
|
+
type: "image",
|
|
179
|
+
content: img.data,
|
|
180
|
+
mimeType: img.mimeType,
|
|
181
|
+
fileName: `image-${idx}`,
|
|
182
|
+
size: Math.ceil(img.data.length * 0.75),
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
await Effect.runPromise(
|
|
186
|
+
sessionOrchestrator.submitPromptAndWait(textContent, {
|
|
187
|
+
mode: "followUp",
|
|
188
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return cancelled ? "cancelled" : "end_turn"
|
|
193
|
+
} finally {
|
|
194
|
+
unsubscribe?.()
|
|
195
|
+
unsubscribe = null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function handleSlashCommand(line: string): StopReason {
|
|
200
|
+
const parts = line.slice(1).split(/\s+/)
|
|
201
|
+
const cmd = parts[0]?.toLowerCase()
|
|
202
|
+
const args = parts.slice(1).join(" ")
|
|
203
|
+
|
|
204
|
+
switch (cmd) {
|
|
205
|
+
case "model":
|
|
206
|
+
if (args && setModel(args)) {
|
|
207
|
+
emitter.emit(textChunk(`Model switched to ${args}`))
|
|
208
|
+
} else if (args) {
|
|
209
|
+
emitter.emit(textChunk(`Unknown model: ${args}. Available: ${models.map((m) => m.modelId).join(", ")}`))
|
|
210
|
+
} else {
|
|
211
|
+
emitter.emit(textChunk(`Current model: ${currentModelId}\nAvailable: ${models.map((m) => m.modelId).join(", ")}`))
|
|
212
|
+
}
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
case "thinking": {
|
|
216
|
+
const levels = ["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
217
|
+
if (levels.includes(args)) {
|
|
218
|
+
agent.setThinkingLevel(args as "off" | "minimal" | "low" | "medium" | "high" | "xhigh")
|
|
219
|
+
thinkingLevel = args
|
|
220
|
+
emitter.emit(textChunk(`Thinking level set to ${args}`))
|
|
221
|
+
} else {
|
|
222
|
+
emitter.emit(textChunk(`Invalid thinking level. Use: ${levels.join(", ")}`))
|
|
223
|
+
}
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case "status": {
|
|
228
|
+
const provider = currentModelId.includes("/") ? currentModelId.split("/")[0] : "anthropic"
|
|
229
|
+
const fmt = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
|
|
230
|
+
let ctx = `0/${fmt(contextWindow)}`
|
|
231
|
+
let cache = ""
|
|
232
|
+
if (lastUsage && contextWindow > 0) {
|
|
233
|
+
const pct = ((lastUsage.totalTokens / contextWindow) * 100).toFixed(1)
|
|
234
|
+
ctx = `${fmt(lastUsage.totalTokens)}/${fmt(contextWindow)} (${pct}%)`
|
|
235
|
+
if (lastUsage.cacheRead) cache = ` | cache: ${fmt(lastUsage.cacheRead)} read`
|
|
236
|
+
}
|
|
237
|
+
const status = `${currentModelId} (${provider}) | ${thinkingLevel} | ${ctx}${cache} | turns: ${turnCount}`
|
|
238
|
+
emitter.emit(textChunk(status))
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case "clear":
|
|
243
|
+
agent.reset()
|
|
244
|
+
emitter.emit(textChunk("Conversation cleared"))
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
case "compact":
|
|
248
|
+
emitter.emit(textChunk("Compact not yet implemented in ACP mode"))
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
default:
|
|
252
|
+
emitter.emit(textChunk(`Unknown command: /${cmd}`))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return "end_turn"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cancel(): void {
|
|
259
|
+
cancelled = true
|
|
260
|
+
agent.abort()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getAvailableCommands(): SlashCommand[] {
|
|
264
|
+
return AVAILABLE_COMMANDS
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getModels(): { options: ModelOption[]; currentModelId: string } {
|
|
268
|
+
return { options: models, currentModelId }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function setModel(modelId: string): boolean {
|
|
272
|
+
const success = config.setModel(modelId)
|
|
273
|
+
if (success) {
|
|
274
|
+
currentModelId = modelId
|
|
275
|
+
emitter.emitModels(models, currentModelId)
|
|
276
|
+
}
|
|
277
|
+
return success
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
id: sessionId,
|
|
282
|
+
cwd,
|
|
283
|
+
prompt,
|
|
284
|
+
cancel,
|
|
285
|
+
getAvailableCommands,
|
|
286
|
+
getModels,
|
|
287
|
+
setModel,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP session/update notification emitter
|
|
3
|
+
* Bridges agent events to ACP protocol notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SessionUpdate,
|
|
8
|
+
SessionUpdateParams,
|
|
9
|
+
SlashCommand,
|
|
10
|
+
ModelOption,
|
|
11
|
+
ToolCallKind,
|
|
12
|
+
ToolCallStatus,
|
|
13
|
+
JsonRpcNotification,
|
|
14
|
+
} from "./protocol.js"
|
|
15
|
+
import { makeNotification } from "./protocol.js"
|
|
16
|
+
|
|
17
|
+
export interface UpdateEmitter {
|
|
18
|
+
emit(update: SessionUpdate): void
|
|
19
|
+
emitCommands(commands: SlashCommand[]): void
|
|
20
|
+
emitModels(availableModels: ModelOption[], currentModelId: string): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createUpdateEmitter(
|
|
24
|
+
sessionId: string,
|
|
25
|
+
write: (notification: JsonRpcNotification) => void
|
|
26
|
+
): UpdateEmitter {
|
|
27
|
+
const emit = (update: SessionUpdate): void => {
|
|
28
|
+
const params: SessionUpdateParams = { sessionId, update }
|
|
29
|
+
write(makeNotification("session/update", params))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
emit,
|
|
34
|
+
|
|
35
|
+
emitCommands(commands: SlashCommand[]): void {
|
|
36
|
+
emit({ sessionUpdate: "available_commands_update", availableCommands: commands })
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
emitModels(availableModels: ModelOption[], currentModelId: string): void {
|
|
40
|
+
emit({ sessionUpdate: "models_update", models: { availableModels, currentModelId } })
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Convenience builders for common updates
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export function textChunk(text: string): SessionUpdate {
|
|
50
|
+
return { sessionUpdate: "agent_message_chunk", content: { type: "text", text } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function thoughtChunk(text: string): SessionUpdate {
|
|
54
|
+
return { sessionUpdate: "agent_thought_chunk", content: { type: "text", text } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function toolCall(
|
|
58
|
+
toolCallId: string,
|
|
59
|
+
title: string,
|
|
60
|
+
kind: ToolCallKind,
|
|
61
|
+
rawInput?: unknown
|
|
62
|
+
): SessionUpdate {
|
|
63
|
+
return {
|
|
64
|
+
sessionUpdate: "tool_call",
|
|
65
|
+
toolCallId,
|
|
66
|
+
title,
|
|
67
|
+
kind,
|
|
68
|
+
status: "pending",
|
|
69
|
+
rawInput,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toolCallUpdate(
|
|
74
|
+
toolCallId: string,
|
|
75
|
+
status: ToolCallStatus,
|
|
76
|
+
content?: string,
|
|
77
|
+
title?: string
|
|
78
|
+
): SessionUpdate {
|
|
79
|
+
return {
|
|
80
|
+
sessionUpdate: "tool_call_update",
|
|
81
|
+
toolCallId,
|
|
82
|
+
status,
|
|
83
|
+
title,
|
|
84
|
+
content: content ? [{ type: "text" as const, text: content }] : undefined,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Map tool names to ACP kinds
|
|
89
|
+
export function toolNameToKind(name: string): ToolCallKind {
|
|
90
|
+
const lower = name.toLowerCase()
|
|
91
|
+
if (lower === "read" || lower.includes("read")) return "read"
|
|
92
|
+
if (lower === "write" || lower.includes("write")) return "write"
|
|
93
|
+
if (lower === "edit" || lower.includes("edit")) return "edit"
|
|
94
|
+
if (lower === "bash" || lower === "command" || lower.includes("terminal")) return "command"
|
|
95
|
+
return "other"
|
|
96
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Message, TextContent } from "@yeshwanthyk/ai"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { createRuntime, type RuntimeInitArgs } from "@runtime/factory.js"
|
|
4
|
+
|
|
5
|
+
const readStdin = async (): Promise<string> => {
|
|
6
|
+
const chunks: Buffer[] = []
|
|
7
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk))
|
|
8
|
+
return Buffer.concat(chunks).toString("utf8")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const textFromBlocks = (blocks: Array<{ type: string }>): string => {
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
for (const block of blocks) {
|
|
14
|
+
if (block.type === "text") parts.push((block as TextContent).text)
|
|
15
|
+
}
|
|
16
|
+
return parts.join("")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const renderMessage = (message: Message): string => {
|
|
20
|
+
if (message.role === "user") {
|
|
21
|
+
if (typeof message.content === "string") return message.content
|
|
22
|
+
return textFromBlocks(message.content)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (message.role === "assistant") {
|
|
26
|
+
const parts: string[] = []
|
|
27
|
+
for (const block of message.content) {
|
|
28
|
+
if (block.type === "text") parts.push(block.text)
|
|
29
|
+
}
|
|
30
|
+
return parts.join("")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return textFromBlocks(message.content)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface HeadlessArgs extends RuntimeInitArgs {
|
|
37
|
+
prompt?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const runHeadless = async (args: HeadlessArgs) => {
|
|
41
|
+
const runtime = await createRuntime(
|
|
42
|
+
{
|
|
43
|
+
configDir: args.configDir,
|
|
44
|
+
configPath: args.configPath,
|
|
45
|
+
provider: args.provider,
|
|
46
|
+
model: args.model,
|
|
47
|
+
thinking: args.thinking,
|
|
48
|
+
},
|
|
49
|
+
"headless",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Initialize hooks with no-op handlers for headless mode (single-shot, no UI)
|
|
53
|
+
runtime.hookRunner.initialize({
|
|
54
|
+
sendHandler: () => {},
|
|
55
|
+
sendMessageHandler: () => {},
|
|
56
|
+
sendUserMessageHandler: async () => {},
|
|
57
|
+
steerHandler: async () => {},
|
|
58
|
+
followUpHandler: async () => {},
|
|
59
|
+
isIdleHandler: () => true,
|
|
60
|
+
appendEntryHandler: (customType, data) => runtime.sessionManager.appendEntry(customType, data),
|
|
61
|
+
getSessionId: () => runtime.sessionManager.sessionId,
|
|
62
|
+
getModel: () => runtime.agent.state.model,
|
|
63
|
+
hasUI: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const prompt = (args.prompt ?? (await readStdin())).trim()
|
|
67
|
+
if (!prompt) {
|
|
68
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "Empty prompt" }) + "\n")
|
|
69
|
+
process.exitCode = 2
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await Effect.runPromise(
|
|
75
|
+
runtime.sessionOrchestrator.submitPromptAndWait(prompt, { mode: "followUp" }),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const conversation = runtime.agent.state.messages.filter((m): m is Message => {
|
|
79
|
+
const role = (m as { role?: unknown }).role
|
|
80
|
+
return role === "user" || role === "assistant" || role === "toolResult"
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const lastAssistant = [...conversation].reverse().find((m) => m.role === "assistant")
|
|
84
|
+
const assistant = lastAssistant ? renderMessage(lastAssistant) : ""
|
|
85
|
+
|
|
86
|
+
process.stdout.write(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
ok: true,
|
|
89
|
+
provider: runtime.config.provider,
|
|
90
|
+
model: runtime.config.modelId,
|
|
91
|
+
prompt,
|
|
92
|
+
assistant,
|
|
93
|
+
validationIssues: runtime.validationIssues,
|
|
94
|
+
}) + "\n",
|
|
95
|
+
)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
ok: false,
|
|
100
|
+
provider: runtime.config.provider,
|
|
101
|
+
model: runtime.config.modelId,
|
|
102
|
+
prompt,
|
|
103
|
+
assistant: "",
|
|
104
|
+
error: err instanceof Error ? err.message : String(err),
|
|
105
|
+
validationIssues: runtime.validationIssues,
|
|
106
|
+
}) + "\n",
|
|
107
|
+
)
|
|
108
|
+
process.exitCode = 1
|
|
109
|
+
} finally {
|
|
110
|
+
await runtime.close().catch(() => {})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createToolRegistry } from "@yeshwanthyk/base-tools"
|
|
2
|
+
import { loadAppConfig } from "../../config.js"
|
|
3
|
+
import { loadCustomCommands } from "@yeshwanthyk/runtime-effect/extensibility/custom-commands.js"
|
|
4
|
+
import type { RuntimeInitArgs } from "@runtime/factory.js"
|
|
5
|
+
import { loadExtensibility } from "@yeshwanthyk/runtime-effect/extensibility/index.js"
|
|
6
|
+
import { formatValidationIssue, hasBlockingIssues } from "@ext/validation.js"
|
|
7
|
+
import type { ValidationIssue } from "@ext/schema.js"
|
|
8
|
+
import type { SendRef } from "@yeshwanthyk/runtime-effect/extensibility/custom-tools/types.js"
|
|
9
|
+
import { SessionManager } from "../../session-manager.js"
|
|
10
|
+
|
|
11
|
+
export const runValidate = async (args: RuntimeInitArgs = {}): Promise<void> => {
|
|
12
|
+
const loaded = await loadAppConfig({
|
|
13
|
+
configDir: args.configDir,
|
|
14
|
+
configPath: args.configPath,
|
|
15
|
+
provider: args.provider,
|
|
16
|
+
model: args.model,
|
|
17
|
+
thinking: args.thinking,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { issues: commandIssues } = loadCustomCommands(loaded.configDir)
|
|
21
|
+
const sendRef: SendRef = { current: () => {} }
|
|
22
|
+
const sessionManager = new SessionManager(loaded.configDir)
|
|
23
|
+
|
|
24
|
+
const builtinToolNames = Object.keys(createToolRegistry(process.cwd()))
|
|
25
|
+
|
|
26
|
+
const extensibility = await loadExtensibility({
|
|
27
|
+
configDir: loaded.configDir,
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
sendRef,
|
|
30
|
+
builtinToolNames,
|
|
31
|
+
hasUI: false,
|
|
32
|
+
sessionManager,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const issues: ValidationIssue[] = [...commandIssues, ...extensibility.validationIssues]
|
|
36
|
+
|
|
37
|
+
if (issues.length === 0) {
|
|
38
|
+
process.stdout.write("No validation issues found.\n")
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdout.write(`Found ${issues.length} validation issue${issues.length === 1 ? "" : "s"}:\n`)
|
|
43
|
+
for (const issue of issues) {
|
|
44
|
+
process.stdout.write(`${formatValidationIssue(issue)}\n`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hasBlockingIssues(issues)) {
|
|
48
|
+
process.exitCode = 1
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { render } from "@opentui/solid"
|
|
2
|
+
import { RuntimeProvider } from "../../runtime/context.js"
|
|
3
|
+
import { createRuntime, type RuntimeInitArgs } from "@runtime/factory.js"
|
|
4
|
+
import type { LoadedSession } from "../../session-manager.js"
|
|
5
|
+
import { selectSession as selectSessionOpen } from "../../session-picker.js"
|
|
6
|
+
import { TuiApp } from "@ui/app-shell/TuiApp.js"
|
|
7
|
+
|
|
8
|
+
interface RunTuiArgs extends RuntimeInitArgs {
|
|
9
|
+
continueSession?: boolean
|
|
10
|
+
resumeSession?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const runTuiOpen = async (args?: RunTuiArgs) => {
|
|
14
|
+
const runtime = await createRuntime(args, "tui")
|
|
15
|
+
const { sessionManager } = runtime
|
|
16
|
+
let initialSession: LoadedSession | null = null
|
|
17
|
+
|
|
18
|
+
if (args?.resumeSession) {
|
|
19
|
+
const selectedPath = await selectSessionOpen(sessionManager)
|
|
20
|
+
if (selectedPath === null) {
|
|
21
|
+
process.stdout.write("No session selected\n")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
initialSession = sessionManager.loadSession(selectedPath)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (args?.continueSession && !initialSession) {
|
|
28
|
+
initialSession = sessionManager.loadLatest()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
() => (
|
|
33
|
+
<RuntimeProvider runtime={runtime}>
|
|
34
|
+
<TuiApp initialSession={initialSession} />
|
|
35
|
+
</RuntimeProvider>
|
|
36
|
+
),
|
|
37
|
+
{ targetFps: 30, exitOnCtrlC: false, useKittyKeyboard: {} },
|
|
38
|
+
)
|
|
39
|
+
}
|