@jmoyers/harness 0.1.9 → 0.1.10
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/README.md +33 -156
- package/package.json +3 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +103 -3
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +6 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server.ts +382 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
|
@@ -6,6 +6,7 @@ export const DEFAULT_GATEWAY_PORT = 7777;
|
|
|
6
6
|
export const DEFAULT_GATEWAY_DB_PATH = '.harness/control-plane.sqlite';
|
|
7
7
|
export const DEFAULT_GATEWAY_RECORD_PATH = '.harness/gateway.json';
|
|
8
8
|
export const DEFAULT_GATEWAY_LOG_PATH = '.harness/gateway.log';
|
|
9
|
+
export const DEFAULT_GATEWAY_LOCK_PATH = '.harness/gateway.lock';
|
|
9
10
|
|
|
10
11
|
export interface GatewayRecord {
|
|
11
12
|
readonly version: number;
|
|
@@ -16,6 +17,7 @@ export interface GatewayRecord {
|
|
|
16
17
|
readonly stateDbPath: string;
|
|
17
18
|
readonly startedAt: string;
|
|
18
19
|
readonly workspaceRoot: string;
|
|
20
|
+
readonly gatewayRunId?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -74,6 +76,13 @@ export function resolveGatewayLogPath(
|
|
|
74
76
|
return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOG_PATH, env);
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export function resolveGatewayLockPath(
|
|
80
|
+
workspaceRoot: string,
|
|
81
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
82
|
+
): string {
|
|
83
|
+
return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOCK_PATH, env);
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
export function normalizeGatewayHost(
|
|
78
87
|
input: string | null | undefined,
|
|
79
88
|
fallback = DEFAULT_GATEWAY_HOST,
|
|
@@ -149,6 +158,9 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
149
158
|
const workspaceRoot = readNonEmptyString(record['workspaceRoot']);
|
|
150
159
|
const authTokenRaw = record['authToken'];
|
|
151
160
|
const authToken = authTokenRaw === null ? null : readNonEmptyString(authTokenRaw);
|
|
161
|
+
const gatewayRunIdRaw = record['gatewayRunId'];
|
|
162
|
+
const gatewayRunId =
|
|
163
|
+
gatewayRunIdRaw === undefined ? undefined : readNonEmptyString(gatewayRunIdRaw);
|
|
152
164
|
|
|
153
165
|
if (
|
|
154
166
|
pid === null ||
|
|
@@ -157,10 +169,12 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
157
169
|
stateDbPath === null ||
|
|
158
170
|
startedAt === null ||
|
|
159
171
|
workspaceRoot === null ||
|
|
160
|
-
(authToken === null && authTokenRaw !== null)
|
|
172
|
+
(authToken === null && authTokenRaw !== null) ||
|
|
173
|
+
(gatewayRunIdRaw !== undefined && gatewayRunId === null)
|
|
161
174
|
) {
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
177
|
+
const parsedGatewayRunId = gatewayRunId === null ? undefined : gatewayRunId;
|
|
164
178
|
|
|
165
179
|
return {
|
|
166
180
|
version,
|
|
@@ -171,6 +185,7 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
171
185
|
stateDbPath,
|
|
172
186
|
startedAt,
|
|
173
187
|
workspaceRoot,
|
|
188
|
+
...(parsedGatewayRunId === undefined ? {} : { gatewayRunId: parsedGatewayRunId }),
|
|
174
189
|
};
|
|
175
190
|
}
|
|
176
191
|
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createAnthropic, generateText } from '../../../packages/harness-ai/src/index.ts';
|
|
2
|
+
import type { StreamSessionPromptRecord } from '../stream-protocol.ts';
|
|
3
|
+
|
|
4
|
+
const THREAD_TITLE_ADAPTER_STATE_KEY = 'harnessThreadTitle';
|
|
5
|
+
const MAX_SANITIZED_PROMPT_CHARS = 1200;
|
|
6
|
+
const IMAGE_DATA_URL_PATTERN = /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+/giu;
|
|
7
|
+
const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*\]\([^)]*\)/gu;
|
|
8
|
+
const HTML_IMAGE_PATTERN = /<img\b[^>]*>/giu;
|
|
9
|
+
const LONG_BASE64_LINE_PATTERN = /^[A-Za-z0-9+/=\s]{160,}$/u;
|
|
10
|
+
const TITLE_WORD_PATTERN = /[A-Za-z0-9]+(?:'[A-Za-z0-9]+)*/g;
|
|
11
|
+
const TARGET_TITLE_WORD_COUNT = 2;
|
|
12
|
+
const FALLBACK_FILL_WORDS = ['current', 'thread'] as const;
|
|
13
|
+
const FALLBACK_STOP_WORDS = new Set([
|
|
14
|
+
'a',
|
|
15
|
+
'an',
|
|
16
|
+
'and',
|
|
17
|
+
'are',
|
|
18
|
+
'as',
|
|
19
|
+
'at',
|
|
20
|
+
'be',
|
|
21
|
+
'build',
|
|
22
|
+
'for',
|
|
23
|
+
'from',
|
|
24
|
+
'in',
|
|
25
|
+
'into',
|
|
26
|
+
'is',
|
|
27
|
+
'it',
|
|
28
|
+
'of',
|
|
29
|
+
'on',
|
|
30
|
+
'or',
|
|
31
|
+
'that',
|
|
32
|
+
'the',
|
|
33
|
+
'this',
|
|
34
|
+
'to',
|
|
35
|
+
'up',
|
|
36
|
+
'with',
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_HAIKU_MODEL_ID = 'claude-3-5-haiku-latest';
|
|
39
|
+
|
|
40
|
+
interface ThreadTitlePromptHistoryEntry {
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly observedAt: string;
|
|
43
|
+
readonly hash: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ThreadTitleNamerInput {
|
|
47
|
+
readonly conversationId: string;
|
|
48
|
+
readonly agentType: string;
|
|
49
|
+
readonly currentTitle: string;
|
|
50
|
+
readonly promptHistory: readonly ThreadTitlePromptHistoryEntry[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ThreadTitleNamer {
|
|
54
|
+
suggest(input: ThreadTitleNamerInput): Promise<string | null>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface AnthropicThreadTitleNamerOptions {
|
|
58
|
+
readonly apiKey: string;
|
|
59
|
+
readonly modelId?: string;
|
|
60
|
+
readonly baseUrl?: string;
|
|
61
|
+
readonly fetch?: typeof fetch;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
65
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return value as Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trimmedString(value: unknown): string | null {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function historyEntriesFromAdapterState(
|
|
80
|
+
adapterState: Record<string, unknown>,
|
|
81
|
+
): readonly ThreadTitlePromptHistoryEntry[] {
|
|
82
|
+
const stored = asRecord(adapterState[THREAD_TITLE_ADAPTER_STATE_KEY]);
|
|
83
|
+
if (stored === null) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const promptsRaw = stored['prompts'];
|
|
87
|
+
if (!Array.isArray(promptsRaw)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const parsed: ThreadTitlePromptHistoryEntry[] = [];
|
|
91
|
+
for (const item of promptsRaw) {
|
|
92
|
+
const record = asRecord(item);
|
|
93
|
+
if (record === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const text = trimmedString(record['text']);
|
|
97
|
+
const observedAt = trimmedString(record['observedAt']);
|
|
98
|
+
const hash = trimmedString(record['hash']);
|
|
99
|
+
if (text === null || observedAt === null || hash === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
parsed.push({
|
|
103
|
+
text,
|
|
104
|
+
observedAt,
|
|
105
|
+
hash,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function serializePromptHistory(
|
|
112
|
+
entries: readonly ThreadTitlePromptHistoryEntry[],
|
|
113
|
+
): Readonly<Record<string, unknown>> {
|
|
114
|
+
return {
|
|
115
|
+
prompts: entries.map((entry) => ({
|
|
116
|
+
text: entry.text,
|
|
117
|
+
observedAt: entry.observedAt,
|
|
118
|
+
hash: entry.hash,
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizePromptLine(line: string): string | null {
|
|
124
|
+
const withoutDataUrl = line
|
|
125
|
+
.replace(IMAGE_DATA_URL_PATTERN, ' ')
|
|
126
|
+
.replace(MARKDOWN_IMAGE_PATTERN, ' ')
|
|
127
|
+
.replace(HTML_IMAGE_PATTERN, ' ')
|
|
128
|
+
.trim();
|
|
129
|
+
if (withoutDataUrl.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (LONG_BASE64_LINE_PATTERN.test(withoutDataUrl)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return withoutDataUrl;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function sanitizePromptForThreadTitle(text: string): string | null {
|
|
139
|
+
const normalized = text
|
|
140
|
+
.replace(/\r\n/gu, '\n')
|
|
141
|
+
.replace(/\r/gu, '\n')
|
|
142
|
+
.split('\n')
|
|
143
|
+
.map((line) => sanitizePromptLine(line))
|
|
144
|
+
.filter((line): line is string => line !== null)
|
|
145
|
+
.join('\n')
|
|
146
|
+
.replace(/[ \t]{2,}/gu, ' ')
|
|
147
|
+
.trim();
|
|
148
|
+
if (normalized.length === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (normalized.length <= MAX_SANITIZED_PROMPT_CHARS) {
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
return `${normalized.slice(0, MAX_SANITIZED_PROMPT_CHARS).trimEnd()}...`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeThreadTitleCandidate(value: string): string | null {
|
|
158
|
+
const compact = value.trim();
|
|
159
|
+
if (compact.length === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const words = compact.match(TITLE_WORD_PATTERN)?.map((word) => word.trim()) ?? [];
|
|
163
|
+
if (words.length < TARGET_TITLE_WORD_COUNT) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const filtered = words
|
|
167
|
+
.map((word) => word.toLowerCase())
|
|
168
|
+
.filter((word) => word !== 'title' && word.length > 0);
|
|
169
|
+
if (filtered.length < TARGET_TITLE_WORD_COUNT) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return filtered.slice(0, TARGET_TITLE_WORD_COUNT).join(' ');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function fallbackThreadTitleFromPromptHistory(
|
|
176
|
+
promptHistory: readonly ThreadTitlePromptHistoryEntry[],
|
|
177
|
+
): string {
|
|
178
|
+
const selected: string[] = [];
|
|
179
|
+
for (let index = promptHistory.length - 1; index >= 0; index -= 1) {
|
|
180
|
+
const entry = promptHistory[index];
|
|
181
|
+
if (entry === undefined) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const words = entry.text.match(TITLE_WORD_PATTERN) ?? [];
|
|
185
|
+
for (const rawWord of words) {
|
|
186
|
+
const normalized = rawWord.toLowerCase();
|
|
187
|
+
if (normalized.length < 3 || FALLBACK_STOP_WORDS.has(normalized)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (selected.includes(normalized)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
selected.push(normalized);
|
|
194
|
+
if (selected.length >= TARGET_TITLE_WORD_COUNT) {
|
|
195
|
+
return selected.join(' ');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const fallback of FALLBACK_FILL_WORDS) {
|
|
200
|
+
selected.push(fallback);
|
|
201
|
+
if (selected.length >= TARGET_TITLE_WORD_COUNT) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return selected.slice(0, TARGET_TITLE_WORD_COUNT).join(' ');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function readThreadTitlePromptHistory(
|
|
209
|
+
adapterState: Record<string, unknown>,
|
|
210
|
+
): readonly ThreadTitlePromptHistoryEntry[] {
|
|
211
|
+
return historyEntriesFromAdapterState(adapterState);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function appendThreadTitlePromptHistory(
|
|
215
|
+
adapterState: Record<string, unknown>,
|
|
216
|
+
prompt: StreamSessionPromptRecord,
|
|
217
|
+
): {
|
|
218
|
+
readonly nextAdapterState: Record<string, unknown>;
|
|
219
|
+
readonly promptHistory: readonly ThreadTitlePromptHistoryEntry[];
|
|
220
|
+
readonly added: boolean;
|
|
221
|
+
} {
|
|
222
|
+
const text = prompt.text === null ? null : sanitizePromptForThreadTitle(prompt.text);
|
|
223
|
+
const existing = historyEntriesFromAdapterState(adapterState);
|
|
224
|
+
if (text === null) {
|
|
225
|
+
return {
|
|
226
|
+
nextAdapterState: adapterState,
|
|
227
|
+
promptHistory: existing,
|
|
228
|
+
added: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const nextHistory: ThreadTitlePromptHistoryEntry[] = [
|
|
232
|
+
...existing,
|
|
233
|
+
{
|
|
234
|
+
text,
|
|
235
|
+
observedAt: prompt.observedAt,
|
|
236
|
+
hash: prompt.hash,
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
const nextAdapterState: Record<string, unknown> = {
|
|
240
|
+
...adapterState,
|
|
241
|
+
[THREAD_TITLE_ADAPTER_STATE_KEY]: serializePromptHistory(nextHistory),
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
nextAdapterState,
|
|
245
|
+
promptHistory: nextHistory,
|
|
246
|
+
added: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createAnthropicThreadTitleNamer(
|
|
251
|
+
options: AnthropicThreadTitleNamerOptions,
|
|
252
|
+
): ThreadTitleNamer {
|
|
253
|
+
const anthropic = createAnthropic({
|
|
254
|
+
apiKey: options.apiKey,
|
|
255
|
+
...(options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl }),
|
|
256
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
257
|
+
});
|
|
258
|
+
const model = anthropic(options.modelId ?? DEFAULT_HAIKU_MODEL_ID);
|
|
259
|
+
return {
|
|
260
|
+
async suggest(input: ThreadTitleNamerInput): Promise<string | null> {
|
|
261
|
+
if (input.promptHistory.length === 0) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const promptLines = input.promptHistory.map(
|
|
265
|
+
(entry, index) => `${String(index + 1)}. ${entry.text}`,
|
|
266
|
+
);
|
|
267
|
+
const response = await generateText({
|
|
268
|
+
model,
|
|
269
|
+
system: [
|
|
270
|
+
'You name active coding-agent threads.',
|
|
271
|
+
'Use the full user prompt history to keep titles relevant and fresh.',
|
|
272
|
+
'Stay high-level and avoid low-level implementation details.',
|
|
273
|
+
'Return exactly 2 words in lowercase with no punctuation and no extra text.',
|
|
274
|
+
].join(' '),
|
|
275
|
+
prompt: [
|
|
276
|
+
`Agent: ${input.agentType}`,
|
|
277
|
+
`Current title: ${input.currentTitle}`,
|
|
278
|
+
`Conversation id: ${input.conversationId}`,
|
|
279
|
+
'Prompt history (oldest to newest):',
|
|
280
|
+
...promptLines,
|
|
281
|
+
'Return a new title now.',
|
|
282
|
+
].join('\n'),
|
|
283
|
+
maxOutputTokens: 16,
|
|
284
|
+
temperature: 0,
|
|
285
|
+
});
|
|
286
|
+
const normalized = normalizeThreadTitleCandidate(response.text);
|
|
287
|
+
return normalized ?? fallbackThreadTitleFromPromptHistory(input.promptHistory);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -421,6 +421,17 @@ function parseConversationUpdate(record: CommandRecord): StreamCommand | null {
|
|
|
421
421
|
};
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
function parseConversationTitleRefresh(record: CommandRecord): StreamCommand | null {
|
|
425
|
+
const conversationId = readString(record['conversationId']);
|
|
426
|
+
if (conversationId === null) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
type: 'conversation.title.refresh',
|
|
431
|
+
conversationId,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
424
435
|
function parseConversationDelete(record: CommandRecord): StreamCommand | null {
|
|
425
436
|
const conversationId = readString(record['conversationId']);
|
|
426
437
|
if (conversationId === null) {
|
|
@@ -1628,6 +1639,7 @@ export const DEFAULT_STREAM_COMMAND_PARSERS: StreamCommandParserRegistry = {
|
|
|
1628
1639
|
'conversation.list': parseConversationList,
|
|
1629
1640
|
'conversation.archive': parseConversationArchive,
|
|
1630
1641
|
'conversation.update': parseConversationUpdate,
|
|
1642
|
+
'conversation.title.refresh': parseConversationTitleRefresh,
|
|
1631
1643
|
'conversation.delete': parseConversationDelete,
|
|
1632
1644
|
'repository.upsert': parseRepositoryUpsert,
|
|
1633
1645
|
'repository.get': parseRepositoryGet,
|
|
@@ -119,6 +119,11 @@ interface ConversationUpdateCommand {
|
|
|
119
119
|
title: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
interface ConversationTitleRefreshCommand {
|
|
123
|
+
type: 'conversation.title.refresh';
|
|
124
|
+
conversationId: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
interface ConversationDeleteCommand {
|
|
123
128
|
type: 'conversation.delete';
|
|
124
129
|
conversationId: string;
|
|
@@ -494,6 +499,7 @@ export type StreamCommand =
|
|
|
494
499
|
| ConversationListCommand
|
|
495
500
|
| ConversationArchiveCommand
|
|
496
501
|
| ConversationUpdateCommand
|
|
502
|
+
| ConversationTitleRefreshCommand
|
|
497
503
|
| ConversationDeleteCommand
|
|
498
504
|
| RepositoryUpsertCommand
|
|
499
505
|
| RepositoryGetCommand
|
|
@@ -419,6 +419,11 @@ interface ExecuteCommandContext {
|
|
|
419
419
|
}>;
|
|
420
420
|
};
|
|
421
421
|
readonly streamCursor: number;
|
|
422
|
+
refreshConversationTitle(conversationId: string): Promise<{
|
|
423
|
+
conversation: ControlPlaneConversationRecord;
|
|
424
|
+
status: 'updated' | 'unchanged' | 'skipped';
|
|
425
|
+
reason: string | null;
|
|
426
|
+
}>;
|
|
422
427
|
refreshGitStatusForDirectory(
|
|
423
428
|
directory: ControlPlaneDirectoryRecord,
|
|
424
429
|
options?: {
|
|
@@ -1403,6 +1408,15 @@ export async function executeStreamServerCommand(
|
|
|
1403
1408
|
};
|
|
1404
1409
|
}
|
|
1405
1410
|
|
|
1411
|
+
if (command.type === 'conversation.title.refresh') {
|
|
1412
|
+
const refreshed = await ctx.refreshConversationTitle(command.conversationId);
|
|
1413
|
+
return {
|
|
1414
|
+
conversation: ctx.conversationRecord(refreshed.conversation),
|
|
1415
|
+
status: refreshed.status,
|
|
1416
|
+
reason: refreshed.reason,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1406
1420
|
if (command.type === 'conversation.delete') {
|
|
1407
1421
|
const existing = ctx.stateStore.getConversation(command.conversationId);
|
|
1408
1422
|
if (existing === null) {
|