novacode 0.6.0 → 0.7.0
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/dist/app-CbJSUNmf.mjs +22 -0
- package/dist/app-CbJSUNmf.mjs.map +1 -0
- package/dist/main.mjs +42 -29
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -2
- package/dist/app-bQ9a_p_K.mjs +0 -22
- package/dist/app-bQ9a_p_K.mjs.map +0 -1
- package/src/agent/agent.ts +0 -87
- package/src/agent/loop.ts +0 -237
- package/src/agent/prompt.ts +0 -50
- package/src/commands/compact.ts +0 -28
- package/src/commands/index.ts +0 -128
- package/src/commands/models.ts +0 -85
- package/src/commands/providers.ts +0 -213
- package/src/commands/session.ts +0 -52
- package/src/config/providers.ts +0 -207
- package/src/config/store.ts +0 -66
- package/src/main.ts +0 -205
- package/src/onboarding/wizard.ts +0 -54
- package/src/provider/gemini.ts +0 -269
- package/src/provider/openai.ts +0 -239
- package/src/provider/stream.ts +0 -138
- package/src/session/compact.ts +0 -159
- package/src/session/store.ts +0 -209
- package/src/tools/fs.ts +0 -189
- package/src/tools/git.ts +0 -99
- package/src/tools/index.ts +0 -33
- package/src/tools/search.ts +0 -274
- package/src/tools/shell.ts +0 -90
- package/src/tools/web.ts +0 -239
- package/src/tui/app.tsx +0 -454
- package/src/tui/components/liveArea.tsx +0 -70
- package/src/tui/components/message.tsx +0 -117
- package/src/tui/components/statusBar.tsx +0 -64
- package/src/tui/constants.ts +0 -25
- package/src/tui/markdown.ts +0 -62
- package/src/tui/prompts.tsx +0 -205
- package/src/types.ts +0 -262
- package/src/update.ts +0 -89
- package/src/util.ts +0 -80
package/src/provider/openai.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AssistantResult,
|
|
3
|
-
Msg,
|
|
4
|
-
StopReason,
|
|
5
|
-
StreamEvent,
|
|
6
|
-
StreamFn,
|
|
7
|
-
StreamOpts,
|
|
8
|
-
ToolDef,
|
|
9
|
-
Usage,
|
|
10
|
-
} from "../types.ts"
|
|
11
|
-
import { EventStream } from "./stream.ts"
|
|
12
|
-
|
|
13
|
-
function msgToOpenAI(msg: Msg): Record<string, unknown> {
|
|
14
|
-
if (msg.role === "user") {
|
|
15
|
-
return {
|
|
16
|
-
role: "user",
|
|
17
|
-
content:
|
|
18
|
-
typeof msg.content === "string"
|
|
19
|
-
? msg.content
|
|
20
|
-
: msg.content.map((c) => {
|
|
21
|
-
if (c.type === "text") return { type: "text", text: c.text }
|
|
22
|
-
if (c.type === "image")
|
|
23
|
-
return { type: "image_url", image_url: { url: `data:${c.mime};base64,${c.data}` } }
|
|
24
|
-
return { type: "text", text: "" }
|
|
25
|
-
}),
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (msg.role === "assistant") {
|
|
29
|
-
const textParts: string[] = []
|
|
30
|
-
const toolCalls: unknown[] = []
|
|
31
|
-
|
|
32
|
-
for (const c of msg.content) {
|
|
33
|
-
if (c.type === "text") textParts.push(c.text)
|
|
34
|
-
// thinking parts are internal — never sent back to the API
|
|
35
|
-
if (c.type === "tool_call")
|
|
36
|
-
toolCalls.push({
|
|
37
|
-
type: "function",
|
|
38
|
-
id: c.id,
|
|
39
|
-
function: { name: c.name, arguments: JSON.stringify(c.args) },
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const result: Record<string, unknown> = {
|
|
44
|
-
role: "assistant",
|
|
45
|
-
content: textParts.length > 0 ? textParts.join("") : null,
|
|
46
|
-
}
|
|
47
|
-
if (toolCalls.length > 0) result.tool_calls = toolCalls
|
|
48
|
-
return result
|
|
49
|
-
}
|
|
50
|
-
// tool_result
|
|
51
|
-
if (msg.role === "tool_result") {
|
|
52
|
-
return {
|
|
53
|
-
role: "tool",
|
|
54
|
-
tool_call_id: msg.callId,
|
|
55
|
-
content: msg.content.map((c) => (c.type === "text" ? c.text : JSON.stringify(c))).join("\n"),
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { role: "user", content: "" }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function toolsToOpenAI(tools: ToolDef[]): unknown[] {
|
|
62
|
-
return tools.map((t) => ({
|
|
63
|
-
type: "function",
|
|
64
|
-
function: {
|
|
65
|
-
name: t.name,
|
|
66
|
-
description: t.description,
|
|
67
|
-
parameters: t.parameters,
|
|
68
|
-
},
|
|
69
|
-
}))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const streamOpenAI: StreamFn = (
|
|
73
|
-
opts: StreamOpts,
|
|
74
|
-
): EventStream<StreamEvent, AssistantResult> => {
|
|
75
|
-
const es = new EventStream<StreamEvent, AssistantResult>()
|
|
76
|
-
|
|
77
|
-
;(async () => {
|
|
78
|
-
let textContent = ""
|
|
79
|
-
const currentToolCalls = new Map<number, { id: string; name: string; args: string }>()
|
|
80
|
-
let usage: Usage = { in: 0, out: 0 }
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const body = {
|
|
84
|
-
model: opts.model.id,
|
|
85
|
-
messages: [{ role: "system", content: opts.system }, ...opts.messages.map(msgToOpenAI)],
|
|
86
|
-
tools: opts.tools.length > 0 ? toolsToOpenAI(opts.tools) : undefined,
|
|
87
|
-
stream: true,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const response = await fetch(`${opts.baseUrl}/chat/completions`, {
|
|
91
|
-
method: "POST",
|
|
92
|
-
headers: {
|
|
93
|
-
"Content-Type": "application/json",
|
|
94
|
-
Authorization: `Bearer ${opts.apiKey}`,
|
|
95
|
-
},
|
|
96
|
-
body: JSON.stringify(body),
|
|
97
|
-
signal: opts.signal,
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
if (!response.ok) {
|
|
101
|
-
const text = await response.text()
|
|
102
|
-
const errorMsg = `API error ${response.status}: ${text}`
|
|
103
|
-
es.push({ type: "text_delta", text: errorMsg })
|
|
104
|
-
es.finish({
|
|
105
|
-
content: [{ type: "text", text: errorMsg }],
|
|
106
|
-
usage: { in: 0, out: 0 },
|
|
107
|
-
stop: "error",
|
|
108
|
-
})
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const reader = response.body?.getReader()
|
|
113
|
-
if (!reader) {
|
|
114
|
-
es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const decoder = new TextDecoder()
|
|
119
|
-
let buffer = ""
|
|
120
|
-
let stop = "stop"
|
|
121
|
-
|
|
122
|
-
while (true) {
|
|
123
|
-
const { done, value } = await reader.read()
|
|
124
|
-
if (done) break
|
|
125
|
-
|
|
126
|
-
buffer += decoder.decode(value, { stream: true })
|
|
127
|
-
const lines = buffer.split("\n")
|
|
128
|
-
buffer = lines.pop() ?? ""
|
|
129
|
-
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
const trimmed = line.trim()
|
|
132
|
-
if (!trimmed?.startsWith("data: ")) continue
|
|
133
|
-
const data = trimmed.slice(6)
|
|
134
|
-
if (data === "[DONE]") continue
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const chunk = JSON.parse(data)
|
|
138
|
-
const delta = chunk.choices?.[0]?.delta
|
|
139
|
-
if (!delta) continue
|
|
140
|
-
|
|
141
|
-
if (delta.content) {
|
|
142
|
-
es.push({ type: "text_delta", text: delta.content })
|
|
143
|
-
textContent += delta.content
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (delta.tool_calls) {
|
|
147
|
-
for (const tc of delta.tool_calls) {
|
|
148
|
-
const idx = tc.index ?? 0
|
|
149
|
-
if (!currentToolCalls.has(idx)) {
|
|
150
|
-
currentToolCalls.set(idx, {
|
|
151
|
-
id: tc.id ?? "",
|
|
152
|
-
name: tc.function?.name ?? "",
|
|
153
|
-
args: "",
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
const existing = currentToolCalls.get(idx)!
|
|
157
|
-
if (tc.id) existing.id = tc.id
|
|
158
|
-
if (tc.function?.name) existing.name = tc.function.name
|
|
159
|
-
if (tc.function?.arguments) existing.args += tc.function.arguments
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (chunk.usage) {
|
|
164
|
-
usage = {
|
|
165
|
-
in: chunk.usage.prompt_tokens ?? 0,
|
|
166
|
-
out: chunk.usage.completion_tokens ?? 0,
|
|
167
|
-
}
|
|
168
|
-
es.push({ type: "usage", usage })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const finishReason = chunk.choices?.[0]?.finish_reason
|
|
172
|
-
if (finishReason) stop = finishReason
|
|
173
|
-
} catch {
|
|
174
|
-
// Skip malformed JSON chunks
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const content: AssistantResult["content"] = []
|
|
180
|
-
if (textContent) {
|
|
181
|
-
content.push({ type: "text", text: textContent })
|
|
182
|
-
}
|
|
183
|
-
for (const [, tc] of currentToolCalls) {
|
|
184
|
-
content.push({
|
|
185
|
-
type: "tool_call",
|
|
186
|
-
id: tc.id,
|
|
187
|
-
name: tc.name,
|
|
188
|
-
args: JSON.parse(tc.args || "{}"),
|
|
189
|
-
})
|
|
190
|
-
es.push({
|
|
191
|
-
type: "tool_call",
|
|
192
|
-
call: {
|
|
193
|
-
type: "tool_call",
|
|
194
|
-
id: tc.id,
|
|
195
|
-
name: tc.name,
|
|
196
|
-
args: JSON.parse(tc.args || "{}"),
|
|
197
|
-
},
|
|
198
|
-
})
|
|
199
|
-
stop = "tool_use"
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
es.finish({ content, usage, stop: stop as StopReason })
|
|
203
|
-
} catch (e) {
|
|
204
|
-
if (opts.signal?.aborted) {
|
|
205
|
-
const content: AssistantResult["content"] = []
|
|
206
|
-
if (textContent) {
|
|
207
|
-
content.push({ type: "text", text: textContent })
|
|
208
|
-
}
|
|
209
|
-
for (const [, tc] of currentToolCalls) {
|
|
210
|
-
try {
|
|
211
|
-
content.push({
|
|
212
|
-
type: "tool_call",
|
|
213
|
-
id: tc.id,
|
|
214
|
-
name: tc.name,
|
|
215
|
-
args: JSON.parse(tc.args || "{}"),
|
|
216
|
-
})
|
|
217
|
-
} catch {
|
|
218
|
-
// skip malformed
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
es.finish({
|
|
222
|
-
content,
|
|
223
|
-
usage,
|
|
224
|
-
stop: "aborted",
|
|
225
|
-
})
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
const errorMsg = `Unexpected error: ${e instanceof Error ? e.message : String(e)}`
|
|
229
|
-
es.push({ type: "text_delta", text: errorMsg })
|
|
230
|
-
es.finish({
|
|
231
|
-
content: [{ type: "text", text: errorMsg }],
|
|
232
|
-
usage: { in: 0, out: 0 },
|
|
233
|
-
stop: "error",
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
})()
|
|
237
|
-
|
|
238
|
-
return es
|
|
239
|
-
}
|
package/src/provider/stream.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { AgentEvent, ApiFormat, AssistantResult, StreamFn, StreamOpts } from "../types.ts"
|
|
2
|
-
import { streamGemini } from "./gemini.ts"
|
|
3
|
-
import { streamOpenAI } from "./openai.ts"
|
|
4
|
-
|
|
5
|
-
export type { AssistantResult, StreamEvent, StreamFn, StreamOpts } from "../types.ts"
|
|
6
|
-
|
|
7
|
-
/*
|
|
8
|
-
* Push-based async event stream.
|
|
9
|
-
*
|
|
10
|
-
* Producers call push()/finish(). Consumers iterate with for-await-of.
|
|
11
|
-
* Backpressure is implicit: push() resolves immediately; the iterator
|
|
12
|
-
* awaits the next value only when the consumer asks for it.
|
|
13
|
-
*/
|
|
14
|
-
export class EventStream<T, R> {
|
|
15
|
-
#events: T[] = []
|
|
16
|
-
#done = false
|
|
17
|
-
#result?: R
|
|
18
|
-
#resolve?: (value: T) => void
|
|
19
|
-
#doneResolve?: (value: R) => void
|
|
20
|
-
#abort = false
|
|
21
|
-
|
|
22
|
-
push(event: T): void {
|
|
23
|
-
if (this.#abort) return
|
|
24
|
-
// If a consumer is already waiting, deliver directly — skip the queue
|
|
25
|
-
if (this.#resolve) {
|
|
26
|
-
const resolve = this.#resolve
|
|
27
|
-
this.#resolve = undefined
|
|
28
|
-
resolve(event)
|
|
29
|
-
} else {
|
|
30
|
-
this.#events.push(event)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
finish(result: R): void {
|
|
35
|
-
this.#done = true
|
|
36
|
-
this.#result = result
|
|
37
|
-
// Wake up a suspended iterator so it can see done=true and exit
|
|
38
|
-
if (this.#resolve) {
|
|
39
|
-
// undefined is a sentinel — the iterator loop checks done after waking
|
|
40
|
-
this.#resolve(undefined as T)
|
|
41
|
-
}
|
|
42
|
-
if (this.#doneResolve) {
|
|
43
|
-
this.#doneResolve(result)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
abort(): void {
|
|
48
|
-
this.#abort = true
|
|
49
|
-
this.#done = true
|
|
50
|
-
if (this.#resolve) {
|
|
51
|
-
this.#resolve(undefined as T)
|
|
52
|
-
}
|
|
53
|
-
if (this.#doneResolve) {
|
|
54
|
-
this.#doneResolve(undefined as R)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
|
59
|
-
while (!this.#done || this.#events.length > 0) {
|
|
60
|
-
if (this.#events.length > 0) {
|
|
61
|
-
yield this.#events.shift() as T
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
if (this.#done) break
|
|
65
|
-
const item = await new Promise<T | undefined>((resolve) => {
|
|
66
|
-
this.#resolve = resolve as (value: T) => void
|
|
67
|
-
})
|
|
68
|
-
if (item !== undefined) {
|
|
69
|
-
yield item
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
get result(): R | undefined {
|
|
75
|
-
return this.#result
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
get isDone(): boolean {
|
|
79
|
-
return this.#done
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Internal map of registered provider implementations
|
|
84
|
-
const registry = new Map<ApiFormat, StreamFn>([
|
|
85
|
-
["openai", streamOpenAI],
|
|
86
|
-
["gemini", streamGemini],
|
|
87
|
-
])
|
|
88
|
-
|
|
89
|
-
export function register(api: ApiFormat, fn: StreamFn): void {
|
|
90
|
-
registry.set(api, fn)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Bridges provider-specific StreamEvents into AgentEvents so the loop and TUI deal with one type.
|
|
94
|
-
export function stream(opts: StreamOpts): EventStream<AgentEvent, AssistantResult> {
|
|
95
|
-
const fn = registry.get(opts.api)
|
|
96
|
-
if (!fn) throw new Error(`No provider registered for API format: ${opts.api}`)
|
|
97
|
-
|
|
98
|
-
// Bridge layer: converts provider-specific StreamEvents into the agent's
|
|
99
|
-
// AgentEvent shape, so the loop and TUI only deal with one event type.
|
|
100
|
-
const providerStream = fn(opts)
|
|
101
|
-
const agentStream = new EventStream<AgentEvent, AssistantResult>()
|
|
102
|
-
|
|
103
|
-
;(async () => {
|
|
104
|
-
for await (const event of providerStream) {
|
|
105
|
-
if (event.type === "text_delta") {
|
|
106
|
-
agentStream.push({ type: "text_delta", text: event.text ?? "" })
|
|
107
|
-
} else if (event.type === "thinking_delta") {
|
|
108
|
-
agentStream.push({ type: "thinking_delta", text: event.text ?? "" })
|
|
109
|
-
} else if (event.type === "tool_call" && event.call) {
|
|
110
|
-
agentStream.push({
|
|
111
|
-
type: "tool_call",
|
|
112
|
-
call: {
|
|
113
|
-
type: "tool_call",
|
|
114
|
-
id: event.call.id,
|
|
115
|
-
name: event.call.name,
|
|
116
|
-
args: event.call.args,
|
|
117
|
-
},
|
|
118
|
-
})
|
|
119
|
-
} else if (event.type === "usage" && event.usage) {
|
|
120
|
-
agentStream.push({ type: "usage", usage: event.usage })
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const res = providerStream.result
|
|
125
|
-
if (res) {
|
|
126
|
-
agentStream.finish(res)
|
|
127
|
-
} else {
|
|
128
|
-
// Fallback for unexpected closure
|
|
129
|
-
agentStream.finish({ content: [], usage: { in: 0, out: 0 }, stop: "stop" })
|
|
130
|
-
}
|
|
131
|
-
})()
|
|
132
|
-
|
|
133
|
-
return agentStream
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function getRegisteredApis(): ApiFormat[] {
|
|
137
|
-
return [...registry.keys()]
|
|
138
|
-
}
|
package/src/session/compact.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { getProvider } from "../config/providers.ts"
|
|
2
|
-
import { stream } from "../provider/stream.ts"
|
|
3
|
-
import type { CompactResult, Model, Msg } from "../types.ts"
|
|
4
|
-
import { estimateTokens } from "../util.ts"
|
|
5
|
-
import type { SessionStore } from "./store.ts"
|
|
6
|
-
|
|
7
|
-
const COMPACT_THRESHOLD = 0.8
|
|
8
|
-
const KEEP_RECENT = 10
|
|
9
|
-
|
|
10
|
-
function extractText(msg: Msg): string {
|
|
11
|
-
if (typeof msg.content === "string") return msg.content
|
|
12
|
-
return msg.content
|
|
13
|
-
.filter((c) => c.type === "text")
|
|
14
|
-
.map((c) => (c.type === "text" ? c.text : ""))
|
|
15
|
-
.join("")
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractToolFiles(msg: Msg, toolName: string): string[] {
|
|
19
|
-
if (msg.role !== "tool_result") return []
|
|
20
|
-
if (!("tool" in msg) || msg.tool !== toolName) return []
|
|
21
|
-
const text = extractText(msg)
|
|
22
|
-
// Extract file paths from tool result content
|
|
23
|
-
const lines = text.split("\n")
|
|
24
|
-
return lines.filter((l) => l.trim().length > 0)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function needsCompact(messages: Msg[], contextWindow: number): boolean {
|
|
28
|
-
return estimateTokens(messages) > contextWindow * COMPACT_THRESHOLD
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function compact(
|
|
32
|
-
store: SessionStore,
|
|
33
|
-
sessionId: string,
|
|
34
|
-
messages: Msg[],
|
|
35
|
-
model: Model,
|
|
36
|
-
apiKey: string,
|
|
37
|
-
baseUrl: string,
|
|
38
|
-
): Promise<CompactResult> {
|
|
39
|
-
if (!needsCompact(messages, model.contextWindow)) {
|
|
40
|
-
return { compacted: false, msgsRemoved: 0 }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const old = messages.slice(0, -KEEP_RECENT)
|
|
44
|
-
if (old.length === 0) {
|
|
45
|
-
return { compacted: false, msgsRemoved: 0 }
|
|
46
|
-
}
|
|
47
|
-
const convo = old
|
|
48
|
-
.map((m) => {
|
|
49
|
-
if (m.role === "user") return `User: ${extractText(m)}`
|
|
50
|
-
if (m.role === "assistant") return `Assistant: ${extractText(m)}`
|
|
51
|
-
if (m.role === "tool_result" && "tool" in m)
|
|
52
|
-
return `Tool(${m.tool}): ${extractText(m).slice(0, 200)}`
|
|
53
|
-
return ""
|
|
54
|
-
})
|
|
55
|
-
.join("\n\n")
|
|
56
|
-
|
|
57
|
-
const summary = await generateSummary(convo, model, apiKey, baseUrl)
|
|
58
|
-
if (!summary) {
|
|
59
|
-
return { compacted: false, msgsRemoved: 0 }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const filesRead: string[] = []
|
|
63
|
-
const filesWrote: string[] = []
|
|
64
|
-
for (const m of old) {
|
|
65
|
-
filesRead.push(...extractToolFiles(m, "read"))
|
|
66
|
-
filesRead.push(...extractToolFiles(m, "glob"))
|
|
67
|
-
filesWrote.push(...extractToolFiles(m, "write"))
|
|
68
|
-
filesWrote.push(...extractToolFiles(m, "edit"))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const seqBefore = old.length
|
|
72
|
-
await store.saveCompaction(
|
|
73
|
-
sessionId,
|
|
74
|
-
summary,
|
|
75
|
-
[...new Set(filesRead)],
|
|
76
|
-
[...new Set(filesWrote)],
|
|
77
|
-
seqBefore,
|
|
78
|
-
)
|
|
79
|
-
await store.truncateBeforeSeq(sessionId, seqBefore + 1)
|
|
80
|
-
|
|
81
|
-
// Insert the summary as a user message so the model retains context
|
|
82
|
-
const summaryMsg: Msg = {
|
|
83
|
-
role: "user",
|
|
84
|
-
content: `[Prior context summary]\n${summary}`,
|
|
85
|
-
ts: Date.now(),
|
|
86
|
-
}
|
|
87
|
-
await store.append(sessionId, summaryMsg)
|
|
88
|
-
|
|
89
|
-
return { compacted: true, summary, msgsRemoved: old.length }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function generateSummary(
|
|
93
|
-
convo: string,
|
|
94
|
-
model: Model,
|
|
95
|
-
apiKey: string,
|
|
96
|
-
baseUrl: string,
|
|
97
|
-
): Promise<string | null> {
|
|
98
|
-
const provider = getProvider(model.provider)
|
|
99
|
-
if (!provider) return null
|
|
100
|
-
|
|
101
|
-
const es = stream({
|
|
102
|
-
api: provider.api,
|
|
103
|
-
model,
|
|
104
|
-
apiKey,
|
|
105
|
-
baseUrl,
|
|
106
|
-
system:
|
|
107
|
-
"Summarize this coding session concisely. Cover: what was asked, files touched, what was done, key decisions. Keep it under 300 words.",
|
|
108
|
-
messages: [{ role: "user", content: convo, ts: Date.now() }],
|
|
109
|
-
tools: [],
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
let summary = ""
|
|
113
|
-
for await (const ev of es) {
|
|
114
|
-
if (ev.type === "text_delta" && ev.text) {
|
|
115
|
-
summary += ev.text
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return summary.trim() || null
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function generateSessionTitle(
|
|
123
|
-
messages: Msg[],
|
|
124
|
-
model: Model,
|
|
125
|
-
apiKey: string,
|
|
126
|
-
baseUrl: string,
|
|
127
|
-
): Promise<string | null> {
|
|
128
|
-
const provider = getProvider(model.provider)
|
|
129
|
-
if (!provider) return null
|
|
130
|
-
|
|
131
|
-
const convo = messages
|
|
132
|
-
.slice(0, 4)
|
|
133
|
-
.map((m) => {
|
|
134
|
-
if (m.role === "user") return `User: ${extractText(m)}`
|
|
135
|
-
if (m.role === "assistant") return `Assistant: ${extractText(m)}`
|
|
136
|
-
return ""
|
|
137
|
-
})
|
|
138
|
-
.join("\n")
|
|
139
|
-
|
|
140
|
-
const es = stream({
|
|
141
|
-
api: provider.api,
|
|
142
|
-
model,
|
|
143
|
-
apiKey,
|
|
144
|
-
baseUrl,
|
|
145
|
-
system:
|
|
146
|
-
"Generate a very short, descriptive, and concise title for this coding conversation. Do not use quotes or prefixes like 'Title:'. Max 6 words.",
|
|
147
|
-
messages: [{ role: "user", content: convo, ts: Date.now() }],
|
|
148
|
-
tools: [],
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
let title = ""
|
|
152
|
-
for await (const ev of es) {
|
|
153
|
-
if (ev.type === "text_delta" && ev.text) {
|
|
154
|
-
title += ev.text
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return title.trim().replace(/^["']|["']$/g, "") || null
|
|
159
|
-
}
|