u-foo 2.5.3 → 2.5.5
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/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const BOX = { h: "─", v: "│", tl: "┌", tr: "┐", bl: "└", br: "┘", t: "┬", b: "┴", l: "├", r: "┤", x: "┼" };
|
|
2
|
+
const {
|
|
3
|
+
classifyChatLogLine,
|
|
4
|
+
compactContinuationIndent,
|
|
5
|
+
compactDividerLabel,
|
|
6
|
+
} = require("../../../ui/ink/chatLogModel");
|
|
2
7
|
|
|
3
8
|
function createRenderer(options = {}) {
|
|
4
9
|
const {
|
|
@@ -239,49 +244,31 @@ function createRenderer(options = {}) {
|
|
|
239
244
|
return str.slice(0, i);
|
|
240
245
|
}
|
|
241
246
|
|
|
242
|
-
function classifyLogLine(raw = "") {
|
|
243
|
-
const clean = stripControl(raw)
|
|
244
|
-
.replace(/\{\/?[^{}\n]+\}/g, "")
|
|
245
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
246
|
-
.replace(/`([^`]+)`/g, "$1");
|
|
247
|
-
const trimmed = clean.trim();
|
|
248
|
-
if (!trimmed) return { kind: "spacer", text: " " };
|
|
249
|
-
if (/^[█▀▄ ]+$/.test(trimmed) || /^ufoo chat/i.test(trimmed)) return { kind: "banner", text: clean };
|
|
250
|
-
if (/^───.*───$/.test(trimmed)) return { kind: "divider", text: clean };
|
|
251
|
-
if (/^(error:|✗|failed\b)/i.test(trimmed)) return { kind: "error", marker: "!", speaker: "error", body: clean.replace(/^(error:\s*)/i, "") };
|
|
252
|
-
if (/^(✓|✔|done\b|closed\b)/i.test(trimmed)) return { kind: "success", marker: "✓", body: clean.replace(/^[✓✔]\s*/, "") };
|
|
253
|
-
const dot = clean.match(/^([^·:\n]{1,34})\s+·\s+(.*)$/);
|
|
254
|
-
if (dot) {
|
|
255
|
-
const speaker = dot[1].trim();
|
|
256
|
-
return {
|
|
257
|
-
kind: speaker.toLowerCase() === "ufoo" ? "assistant" : "agent",
|
|
258
|
-
marker: speaker.toLowerCase() === "ufoo" ? "◆" : "●",
|
|
259
|
-
speaker,
|
|
260
|
-
body: dot[2] || " ",
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
const colon = clean.match(/^([A-Za-z0-9_.:@/-]{1,34}):\s+(.*)$/);
|
|
264
|
-
if (colon) return { kind: "agent", marker: "●", speaker: colon[1], body: colon[2] || " " };
|
|
265
|
-
return { kind: "plain", marker: "│", body: clean };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
247
|
function formatChatLogLine(raw = "", width = 80) {
|
|
269
|
-
const row =
|
|
248
|
+
const row = classifyChatLogLine(stripControl(raw));
|
|
270
249
|
const reset = "\x1b[0m";
|
|
271
250
|
if (row.kind === "spacer") return " ".repeat(width);
|
|
272
|
-
if (row.kind === "banner") return `\x1b[36;1m${truncateVisible(row.
|
|
273
|
-
if (row.kind === "divider") return `\x1b[90m${truncateVisible(row.
|
|
251
|
+
if (row.kind === "banner") return `\x1b[36;1m${truncateVisible(row.body, width)}${reset}`;
|
|
252
|
+
if (row.kind === "divider") return `\x1b[90m${truncateVisible(` ${compactDividerLabel(row.body)}`, width)}${reset}`;
|
|
274
253
|
|
|
275
254
|
const palette = {
|
|
276
255
|
assistant: { marker: "\x1b[36m", speaker: "\x1b[37;1m", body: "" },
|
|
277
256
|
agent: { marker: "\x1b[36m", speaker: "\x1b[36m", body: "" },
|
|
278
257
|
error: { marker: "\x1b[31;1m", speaker: "\x1b[31m", body: "\x1b[31m" },
|
|
279
258
|
success: { marker: "\x1b[32m", speaker: "\x1b[32m", body: "\x1b[32m" },
|
|
259
|
+
meta: { marker: "\x1b[90m", speaker: "\x1b[90m", body: "\x1b[90m" },
|
|
280
260
|
plain: { marker: "\x1b[90m", speaker: "\x1b[90m", body: "" },
|
|
281
261
|
};
|
|
282
262
|
const colors = palette[row.kind] || palette.plain;
|
|
283
263
|
const speaker = row.speaker ? `${colors.speaker}${row.speaker}${reset}\x1b[90m · ${reset}` : "";
|
|
284
|
-
const
|
|
264
|
+
const markerGlyph = row.kind === "agent" ? "●" : row.marker;
|
|
265
|
+
const marker = markerGlyph
|
|
266
|
+
? `${colors.marker}${markerGlyph}${reset} `
|
|
267
|
+
: " ";
|
|
268
|
+
const body = row.kind === "plain"
|
|
269
|
+
? compactContinuationIndent(row.body || row.text || " ")
|
|
270
|
+
: (row.body || row.text || " ");
|
|
271
|
+
const line = `${marker}${speaker}${colors.body || ""}${body}${reset}`;
|
|
285
272
|
return truncateVisible(line, width);
|
|
286
273
|
}
|
|
287
274
|
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -23,6 +23,14 @@ const fmt = require("../format");
|
|
|
23
23
|
const { createMultilineInput } = require("./MultilineInput");
|
|
24
24
|
const { createDashboardBar } = require("./DashboardBar");
|
|
25
25
|
const { reducer, createInitialState } = require("./chatReducer");
|
|
26
|
+
const {
|
|
27
|
+
stripBlessedTags,
|
|
28
|
+
compactDividerLabel,
|
|
29
|
+
classifyChatLogLine,
|
|
30
|
+
buildChatLogLineModel,
|
|
31
|
+
buildChatLogGroups,
|
|
32
|
+
chatLogEntryText,
|
|
33
|
+
} = require("./chatLogModel");
|
|
26
34
|
const { restartDaemonLifecycle } = require("../../runtime/daemon/restart");
|
|
27
35
|
|
|
28
36
|
function bootstrapEnvironment(projectRoot, options = {}) {
|
|
@@ -157,12 +165,20 @@ function loadChatHistory(projectRoot, cap = 200, options = {}) {
|
|
|
157
165
|
const raw = fs.readFileSync(file, "utf8");
|
|
158
166
|
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
159
167
|
const out = [];
|
|
168
|
+
const pushLine = (line = "") => {
|
|
169
|
+
const value = String(line || "");
|
|
170
|
+
if (!value.trim()) {
|
|
171
|
+
if (out.length > 0 && out[out.length - 1] !== "") out.push("");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
out.push(value);
|
|
175
|
+
};
|
|
160
176
|
for (const line of lines) {
|
|
161
177
|
try {
|
|
162
178
|
const entry = JSON.parse(line);
|
|
163
179
|
if (!entry) continue;
|
|
164
180
|
if (entry.type === "spacer") {
|
|
165
|
-
|
|
181
|
+
pushLine("");
|
|
166
182
|
continue;
|
|
167
183
|
}
|
|
168
184
|
const text = String(entry.text || "");
|
|
@@ -170,12 +186,18 @@ function loadChatHistory(projectRoot, cap = 200, options = {}) {
|
|
|
170
186
|
// Strip blessed-tag markup that the legacy log writer used; ink
|
|
171
187
|
// can't render those tags and we don't want them shown literally.
|
|
172
188
|
const stripped = text.replace(/\{[^{}]+\}/g, "");
|
|
173
|
-
|
|
189
|
+
for (const renderedLine of normalizeInkLogLines(stripped)) {
|
|
190
|
+
pushLine(renderedLine);
|
|
191
|
+
}
|
|
174
192
|
} catch {
|
|
175
193
|
// ignore malformed lines
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
|
-
|
|
196
|
+
while (out.length > 0 && out[0] === "") out.shift();
|
|
197
|
+
while (out.length > 0 && out[out.length - 1] === "") out.pop();
|
|
198
|
+
const capped = out.slice(-cap);
|
|
199
|
+
while (capped.length > 0 && capped[0] === "") capped.shift();
|
|
200
|
+
return capped;
|
|
179
201
|
} catch {
|
|
180
202
|
return [];
|
|
181
203
|
}
|
|
@@ -369,67 +391,11 @@ function buildPromptIpcRequest(text) {
|
|
|
369
391
|
};
|
|
370
392
|
}
|
|
371
393
|
|
|
372
|
-
function stripBlessedTags(text = "") {
|
|
373
|
-
return String(text || "")
|
|
374
|
-
.replace(/\{\/?[^{}\n]+\}/g, "")
|
|
375
|
-
.replace(/\r/g, "");
|
|
376
|
-
}
|
|
377
|
-
|
|
378
394
|
function normalizeInkLogLines(text = "") {
|
|
379
395
|
const clean = stripBlessedTags(text);
|
|
380
396
|
return clean.split(/\r?\n/);
|
|
381
397
|
}
|
|
382
398
|
|
|
383
|
-
function stripMarkdownDecorators(text = "") {
|
|
384
|
-
return String(text || "")
|
|
385
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
386
|
-
.replace(/`([^`]+)`/g, "$1");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function classifyChatLogLine(text = "") {
|
|
390
|
-
const raw = stripBlessedTags(text).replace(/\r/g, "");
|
|
391
|
-
const clean = stripMarkdownDecorators(raw);
|
|
392
|
-
const trimmed = clean.trim();
|
|
393
|
-
if (!trimmed) return { kind: "spacer", marker: " ", speaker: "", body: " " };
|
|
394
|
-
if (/^[█▀▄ ]+(?:\s{2,}(?:Version|Mode|Dictionary):.*)?$/.test(trimmed) || /^ufoo chat/i.test(trimmed)) {
|
|
395
|
-
return { kind: "banner", marker: " ", speaker: "", body: clean };
|
|
396
|
-
}
|
|
397
|
-
if (/^───.*───$/.test(trimmed)) {
|
|
398
|
-
return { kind: "divider", marker: "─", speaker: "", body: clean };
|
|
399
|
-
}
|
|
400
|
-
if (/^(error:|✗|failed\b)/i.test(trimmed)) {
|
|
401
|
-
return { kind: "error", marker: "!", speaker: "error", body: clean.replace(/^(error:\s*)/i, "") };
|
|
402
|
-
}
|
|
403
|
-
if (/^(✓|✔|done\b|closed\b)/i.test(trimmed)) {
|
|
404
|
-
return { kind: "success", marker: "✓", speaker: "", body: clean.replace(/^[✓✔]\s*/, "") };
|
|
405
|
-
}
|
|
406
|
-
const dotMatch = clean.match(/^([^·:\n]{1,42})\s+·\s+(.*)$/);
|
|
407
|
-
if (dotMatch) {
|
|
408
|
-
const speaker = dotMatch[1].trim();
|
|
409
|
-
const lower = speaker.toLowerCase();
|
|
410
|
-
const kind = lower === "ufoo" ? "assistant" : "agent";
|
|
411
|
-
return { kind, marker: kind === "assistant" ? "◆" : "•", speaker, body: dotMatch[2] || " " };
|
|
412
|
-
}
|
|
413
|
-
const colonMatch = clean.match(/^([A-Za-z0-9_.:@/-]{1,42}):\s+(.*)$/);
|
|
414
|
-
if (colonMatch) {
|
|
415
|
-
return { kind: "agent", marker: "•", speaker: colonMatch[1], body: colonMatch[2] || " " };
|
|
416
|
-
}
|
|
417
|
-
if (/^(CHAT|UCODE)\s+·/i.test(trimmed)) {
|
|
418
|
-
return { kind: "meta", marker: "·", speaker: "", body: clean };
|
|
419
|
-
}
|
|
420
|
-
return { kind: "plain", marker: "│", speaker: "", body: clean };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function buildChatLogLineModel(text = "") {
|
|
424
|
-
const row = classifyChatLogLine(text);
|
|
425
|
-
const hasSpeaker = Boolean(row.speaker);
|
|
426
|
-
return {
|
|
427
|
-
...row,
|
|
428
|
-
markerText: hasSpeaker ? `${row.marker || " "} ` : `${row.marker || " "} `,
|
|
429
|
-
bodyText: row.body || " ",
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
399
|
function createInkStreamState({
|
|
434
400
|
dispatch,
|
|
435
401
|
appendHistory,
|
|
@@ -1234,7 +1200,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1234
1200
|
getChatLogLines: () => {
|
|
1235
1201
|
const current = stateRef.current || {};
|
|
1236
1202
|
return Array.isArray(current.logLines)
|
|
1237
|
-
? current.logLines.map((item) =>
|
|
1203
|
+
? current.logLines.map((item) => chatLogEntryText(item))
|
|
1238
1204
|
: [];
|
|
1239
1205
|
},
|
|
1240
1206
|
getStatusText: () => {
|
|
@@ -3262,9 +3228,9 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3262
3228
|
return null;
|
|
3263
3229
|
}
|
|
3264
3230
|
|
|
3265
|
-
const
|
|
3266
|
-
const row =
|
|
3267
|
-
const key =
|
|
3231
|
+
const renderChatLogEntry = (entry, group) => {
|
|
3232
|
+
const row = entry && entry.row ? entry.row : buildChatLogLineModel("");
|
|
3233
|
+
const key = entry && entry.id ? entry.id : `log-${row.body}`;
|
|
3268
3234
|
if (row.kind === "spacer") {
|
|
3269
3235
|
return h(Text, { key, color: "gray" }, " ");
|
|
3270
3236
|
}
|
|
@@ -3281,7 +3247,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3281
3247
|
const colors = palette[row.kind] || palette.plain;
|
|
3282
3248
|
if (row.kind === "divider") {
|
|
3283
3249
|
return h(Box, { key, marginBottom: 1 },
|
|
3284
|
-
h(Text, { color: colors.body, wrap: "truncate" }, row.body),
|
|
3250
|
+
h(Text, { color: colors.body, wrap: "truncate" }, ` ${compactDividerLabel(row.body)}`),
|
|
3285
3251
|
);
|
|
3286
3252
|
}
|
|
3287
3253
|
if (row.kind === "banner") {
|
|
@@ -3289,13 +3255,16 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3289
3255
|
h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
|
|
3290
3256
|
);
|
|
3291
3257
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3258
|
+
const markerText = entry && entry.continuation
|
|
3259
|
+
? (group && (group.kind === "assistant" || group.kind === "agent") ? " " : " ")
|
|
3260
|
+
: row.markerText;
|
|
3261
|
+
return h(Box, { key, width: "100%" },
|
|
3262
|
+
h(Text, { color: colors.marker, bold: row.kind === "error" }, markerText),
|
|
3294
3263
|
h(Text, { color: colors.body, wrap: "wrap" },
|
|
3295
|
-
row.speaker
|
|
3264
|
+
row.speaker && !(entry && entry.continuation)
|
|
3296
3265
|
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3297
3266
|
: null,
|
|
3298
|
-
row.speaker
|
|
3267
|
+
row.speaker && !(entry && entry.continuation)
|
|
3299
3268
|
? h(Text, { color: "gray" }, " · ")
|
|
3300
3269
|
: null,
|
|
3301
3270
|
row.bodyText,
|
|
@@ -3303,6 +3272,27 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3303
3272
|
);
|
|
3304
3273
|
};
|
|
3305
3274
|
|
|
3275
|
+
const renderChatLogGroup = (group) => {
|
|
3276
|
+
const entries = Array.isArray(group && group.entries) ? group.entries : [];
|
|
3277
|
+
if (entries.length === 0) return null;
|
|
3278
|
+
const first = entries[0] || {};
|
|
3279
|
+
const row = first.row || buildChatLogLineModel("");
|
|
3280
|
+
if (row.kind === "spacer" || row.kind === "banner" || row.kind === "divider") {
|
|
3281
|
+
return renderChatLogEntry(first, group);
|
|
3282
|
+
}
|
|
3283
|
+
return h(Box, {
|
|
3284
|
+
key: `group-${group.id}`,
|
|
3285
|
+
flexDirection: "column",
|
|
3286
|
+
width: "100%",
|
|
3287
|
+
marginBottom: 1,
|
|
3288
|
+
},
|
|
3289
|
+
...entries.map((entry) => renderChatLogEntry(entry, group)));
|
|
3290
|
+
};
|
|
3291
|
+
|
|
3292
|
+
const renderChatLogGroups = (items) => buildChatLogGroups(items)
|
|
3293
|
+
.map(renderChatLogGroup)
|
|
3294
|
+
.filter(Boolean);
|
|
3295
|
+
|
|
3306
3296
|
if (state.viewingAgentId) {
|
|
3307
3297
|
const maxWidth = Math.max(20, size.cols || 80);
|
|
3308
3298
|
const logRows = Math.max(1, (size.rows || 24) - 5);
|
|
@@ -3379,7 +3369,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3379
3369
|
|
|
3380
3370
|
return h(Box, { flexDirection: "column", width: "100%" },
|
|
3381
3371
|
h(Box, { flexDirection: "column", width: "100%" },
|
|
3382
|
-
...state.logLines
|
|
3372
|
+
...renderChatLogGroups(state.logLines),
|
|
3383
3373
|
),
|
|
3384
3374
|
state.activeMerge ? h(Box, null,
|
|
3385
3375
|
h(Text, { color: state.activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
|
|
@@ -3391,10 +3381,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3391
3381
|
const prefix = state.activeStream.publisher
|
|
3392
3382
|
? `${state.activeStream.publisher}: `
|
|
3393
3383
|
: "";
|
|
3394
|
-
return lines.map((line, idx) =>
|
|
3384
|
+
return renderChatLogGroups(lines.map((line, idx) => ({
|
|
3395
3385
|
id: `s-${idx}`,
|
|
3396
3386
|
text: idx === 0 ? `${prefix}${line}` : ` ${line}`,
|
|
3397
|
-
}));
|
|
3387
|
+
})));
|
|
3398
3388
|
})(),
|
|
3399
3389
|
) : null,
|
|
3400
3390
|
h(Box, { marginTop: 1, width: "100%" },
|
|
@@ -3680,6 +3670,7 @@ module.exports = {
|
|
|
3680
3670
|
chatHistoryOptionsForScope,
|
|
3681
3671
|
classifyChatLogLine,
|
|
3682
3672
|
buildChatLogLineModel,
|
|
3673
|
+
buildChatLogGroups,
|
|
3683
3674
|
createInkMultiWindowToggle,
|
|
3684
3675
|
resolveActiveAgentId,
|
|
3685
3676
|
resolveInjectSockPathForAgent,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function stripBlessedTags(text = "") {
|
|
4
|
+
return String(text || "")
|
|
5
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
6
|
+
.replace(/\{\/?[^{}\n]+\}/g, "")
|
|
7
|
+
.replace(/\r/g, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripMarkdownDecorators(text = "") {
|
|
11
|
+
return String(text || "")
|
|
12
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
13
|
+
.replace(/`([^`]+)`/g, "$1");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compactContinuationIndent(text = "") {
|
|
17
|
+
return String(text || "").replace(/^\s{8,}(?=\S)/, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compactDividerLabel(text = "") {
|
|
21
|
+
const label = String(text || "")
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^─+\s*/, "")
|
|
24
|
+
.replace(/\s*─+$/, "")
|
|
25
|
+
.trim();
|
|
26
|
+
return label || String(text || "").trim() || "section";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function classifyChatLogLine(text = "") {
|
|
30
|
+
const raw = stripBlessedTags(text).replace(/\r/g, "");
|
|
31
|
+
const clean = stripMarkdownDecorators(raw);
|
|
32
|
+
const trimmed = clean.trim();
|
|
33
|
+
if (!trimmed) return { kind: "spacer", marker: " ", speaker: "", body: " " };
|
|
34
|
+
if (/^[█▀▄ ]+(?:\s{2,}(?:Version|Mode|Dictionary):.*)?$/.test(trimmed) || /^ufoo chat/i.test(trimmed)) {
|
|
35
|
+
return { kind: "banner", marker: " ", speaker: "", body: clean };
|
|
36
|
+
}
|
|
37
|
+
if (/^───.*───$/.test(trimmed)) {
|
|
38
|
+
return { kind: "divider", marker: "─", speaker: "", body: clean };
|
|
39
|
+
}
|
|
40
|
+
if (/^(CHAT|UCODE)\s+·/i.test(trimmed)) {
|
|
41
|
+
return { kind: "meta", marker: "·", speaker: "", body: clean };
|
|
42
|
+
}
|
|
43
|
+
if (/^(error:|✗|failed\b)/i.test(trimmed)) {
|
|
44
|
+
return { kind: "error", marker: "!", speaker: "error", body: clean.replace(/^(error:\s*)/i, "") };
|
|
45
|
+
}
|
|
46
|
+
if (/^(✓|✔|done\b|closed\b)/i.test(trimmed)) {
|
|
47
|
+
return { kind: "success", marker: "✓", speaker: "", body: clean.replace(/^[✓✔]\s*/, "") };
|
|
48
|
+
}
|
|
49
|
+
const dotMatch = clean.match(/^([^·\n]{1,64})\s+·\s+(.*)$/);
|
|
50
|
+
if (dotMatch) {
|
|
51
|
+
const speaker = dotMatch[1].trim();
|
|
52
|
+
const lower = speaker.toLowerCase();
|
|
53
|
+
const kind = lower === "ufoo" ? "assistant" : "agent";
|
|
54
|
+
return { kind, marker: kind === "assistant" ? "◆" : "•", speaker, body: dotMatch[2] || " " };
|
|
55
|
+
}
|
|
56
|
+
const colonMatch = clean.match(/^([A-Za-z0-9_.:@/-]{1,42}):\s+(.*)$/);
|
|
57
|
+
if (colonMatch) {
|
|
58
|
+
return { kind: "agent", marker: "•", speaker: colonMatch[1], body: colonMatch[2] || " " };
|
|
59
|
+
}
|
|
60
|
+
return { kind: "plain", marker: "", speaker: "", body: clean };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function defaultMarkerForKind(kind = "", speaker = "") {
|
|
64
|
+
if (kind === "assistant") return "◆";
|
|
65
|
+
if (kind === "agent") return "•";
|
|
66
|
+
if (kind === "error") return "!";
|
|
67
|
+
if (kind === "success") return "✓";
|
|
68
|
+
if (kind === "divider") return "─";
|
|
69
|
+
if (kind === "meta") return "·";
|
|
70
|
+
if (kind === "banner" || kind === "spacer") return " ";
|
|
71
|
+
return speaker ? "•" : "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildChatLogLineModel(input = "") {
|
|
75
|
+
if (input && typeof input === "object" && !input.kind) {
|
|
76
|
+
return buildChatLogLineModel(chatLogEntryText(input));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (input && typeof input === "object" && input.kind) {
|
|
80
|
+
const kind = String(input.kind || "plain");
|
|
81
|
+
const speaker = String(input.speaker || "");
|
|
82
|
+
const marker = input.marker != null ? String(input.marker) : defaultMarkerForKind(kind, speaker);
|
|
83
|
+
const rawBody = input.bodyText != null
|
|
84
|
+
? String(input.bodyText)
|
|
85
|
+
: String(input.body != null ? input.body : chatLogEntryText(input));
|
|
86
|
+
const body = kind === "plain" ? compactContinuationIndent(rawBody || " ") : (rawBody || " ");
|
|
87
|
+
return {
|
|
88
|
+
kind,
|
|
89
|
+
marker,
|
|
90
|
+
speaker,
|
|
91
|
+
body: input.body != null ? String(input.body) : rawBody,
|
|
92
|
+
markerText: input.markerText != null
|
|
93
|
+
? String(input.markerText)
|
|
94
|
+
: (speaker ? `${marker || " "} ` : `${marker || " "} `),
|
|
95
|
+
bodyText: body,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const row = classifyChatLogLine(input);
|
|
100
|
+
const hasSpeaker = Boolean(row.speaker);
|
|
101
|
+
const body = row.kind === "plain"
|
|
102
|
+
? compactContinuationIndent(row.body || " ")
|
|
103
|
+
: (row.body || " ");
|
|
104
|
+
return {
|
|
105
|
+
...row,
|
|
106
|
+
markerText: hasSpeaker ? `${row.marker || " "} ` : `${row.marker || " "} `,
|
|
107
|
+
bodyText: body,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeEntryInput(input) {
|
|
112
|
+
if (input && typeof input === "object" && !Array.isArray(input)) return input;
|
|
113
|
+
return { text: input };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createChatLogEntry(input = "", id = "") {
|
|
117
|
+
const source = normalizeEntryInput(input);
|
|
118
|
+
const text = String(source.text != null ? source.text : chatLogEntryText(source));
|
|
119
|
+
const row = source.kind
|
|
120
|
+
? buildChatLogLineModel({ ...source, text })
|
|
121
|
+
: buildChatLogLineModel(text);
|
|
122
|
+
const meta = source.meta && typeof source.meta === "object" && !Array.isArray(source.meta)
|
|
123
|
+
? { ...source.meta }
|
|
124
|
+
: {};
|
|
125
|
+
const entry = {
|
|
126
|
+
id: String(id || source.id || ""),
|
|
127
|
+
text,
|
|
128
|
+
kind: row.kind,
|
|
129
|
+
marker: row.marker,
|
|
130
|
+
speaker: row.speaker,
|
|
131
|
+
body: row.body,
|
|
132
|
+
markerText: row.markerText,
|
|
133
|
+
bodyText: row.bodyText,
|
|
134
|
+
sourceType: String(source.sourceType || source.type || ""),
|
|
135
|
+
meta,
|
|
136
|
+
};
|
|
137
|
+
return entry;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function chatLogEntryText(entry = "") {
|
|
141
|
+
if (typeof entry === "string") return entry;
|
|
142
|
+
if (!entry || typeof entry !== "object") return "";
|
|
143
|
+
if (entry.text != null) return String(entry.text);
|
|
144
|
+
if (entry.speaker) return `${entry.speaker} · ${entry.bodyText || entry.body || ""}`;
|
|
145
|
+
return String(entry.bodyText || entry.body || "");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function canAppendToChatLogGroup(group, row) {
|
|
149
|
+
if (!group || !row) return false;
|
|
150
|
+
if (row.kind !== "plain" && row.kind !== "spacer") return false;
|
|
151
|
+
return group.kind === "assistant"
|
|
152
|
+
|| group.kind === "agent"
|
|
153
|
+
|| group.kind === "success"
|
|
154
|
+
|| group.kind === "error"
|
|
155
|
+
|| group.kind === "meta"
|
|
156
|
+
|| group.kind === "plain";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildChatLogGroups(items = []) {
|
|
160
|
+
const source = Array.isArray(items) ? items : [];
|
|
161
|
+
const groups = [];
|
|
162
|
+
let current = null;
|
|
163
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
164
|
+
const item = source[index] || {};
|
|
165
|
+
const itemId = item && typeof item === "object" && item.id ? item.id : `log-${index}`;
|
|
166
|
+
const row = buildChatLogLineModel(item);
|
|
167
|
+
const entry = {
|
|
168
|
+
id: itemId,
|
|
169
|
+
text: chatLogEntryText(item),
|
|
170
|
+
row,
|
|
171
|
+
sourceType: item && typeof item === "object" ? String(item.sourceType || item.type || "") : "",
|
|
172
|
+
meta: item && typeof item === "object" && item.meta ? item.meta : {},
|
|
173
|
+
continuation: false,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (canAppendToChatLogGroup(current, row)) {
|
|
177
|
+
entry.continuation = true;
|
|
178
|
+
current.entries.push(entry);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
current = {
|
|
183
|
+
id: entry.id,
|
|
184
|
+
kind: row.kind,
|
|
185
|
+
entries: [entry],
|
|
186
|
+
};
|
|
187
|
+
groups.push(current);
|
|
188
|
+
}
|
|
189
|
+
return groups;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
stripBlessedTags,
|
|
194
|
+
stripMarkdownDecorators,
|
|
195
|
+
compactContinuationIndent,
|
|
196
|
+
compactDividerLabel,
|
|
197
|
+
classifyChatLogLine,
|
|
198
|
+
buildChatLogLineModel,
|
|
199
|
+
buildChatLogGroups,
|
|
200
|
+
createChatLogEntry,
|
|
201
|
+
chatLogEntryText,
|
|
202
|
+
};
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
const fmt = require("../format");
|
|
38
|
+
const { createChatLogEntry } = require("./chatLogModel");
|
|
38
39
|
|
|
39
40
|
const LOG_CAP = 1000;
|
|
40
41
|
const HISTORY_CAP = 200;
|
|
@@ -100,7 +101,10 @@ function createInitialState({ banner = [], globalMode = false, globalScope = "co
|
|
|
100
101
|
const initialAgentProvider = settings.agentProvider || "codex-cli";
|
|
101
102
|
const selectedProviderIndex = Math.max(0, DEFAULT_PROVIDER_OPTIONS.findIndex((opt) => opt.value === initialAgentProvider));
|
|
102
103
|
return {
|
|
103
|
-
logLines: banner.concat([""]).map((line, idx) => ({
|
|
104
|
+
logLines: banner.concat([""]).map((line, idx) => createChatLogEntry({
|
|
105
|
+
text: line,
|
|
106
|
+
sourceType: "banner",
|
|
107
|
+
}, `b-${idx}`)),
|
|
104
108
|
lineSeq: banner.length + 1,
|
|
105
109
|
draft: "",
|
|
106
110
|
focusMode: "input",
|
|
@@ -144,10 +148,10 @@ function createInitialState({ banner = [], globalMode = false, globalScope = "co
|
|
|
144
148
|
function appendLog(state, lines) {
|
|
145
149
|
const incoming = Array.isArray(lines) ? lines : [lines];
|
|
146
150
|
let seq = state.lineSeq;
|
|
147
|
-
const out = state.logLines.concat(incoming.map((
|
|
151
|
+
const out = state.logLines.concat(incoming.map((line) => {
|
|
148
152
|
const id = `l-${seq}`;
|
|
149
153
|
seq += 1;
|
|
150
|
-
return
|
|
154
|
+
return createChatLogEntry(line, id);
|
|
151
155
|
}));
|
|
152
156
|
return {
|
|
153
157
|
...state,
|