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,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -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 = classifyLogLine(raw);
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.text, width)}${reset}`;
273
- if (row.kind === "divider") return `\x1b[90m${truncateVisible(row.text, width)}${reset}`;
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 line = `${colors.marker}${row.marker || ""}${reset} ${speaker}${colors.body || ""}${row.body || row.text || " "}${reset}`;
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
 
@@ -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
- out.push("");
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
- out.push(stripped);
189
+ for (const renderedLine of normalizeInkLogLines(stripped)) {
190
+ pushLine(renderedLine);
191
+ }
174
192
  } catch {
175
193
  // ignore malformed lines
176
194
  }
177
195
  }
178
- return out.slice(-cap);
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) => String((item && item.text) || ""))
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 renderChatLogLine = (item) => {
3266
- const row = buildChatLogLineModel((item && item.text) || "");
3267
- const key = item && item.id ? item.id : `log-${row.body}`;
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
- return h(Box, { key, width: "100%", marginBottom: 1 },
3293
- h(Text, { color: colors.marker, bold: row.kind === "error" }, row.markerText),
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.map(renderChatLogLine),
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) => renderChatLogLine({
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) => ({ id: `b-${idx}`, text: line })),
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((text) => {
151
+ const out = state.logLines.concat(incoming.map((line) => {
148
152
  const id = `l-${seq}`;
149
153
  seq += 1;
150
- return { id, text: String(text == null ? "" : text) };
154
+ return createChatLogEntry(line, id);
151
155
  }));
152
156
  return {
153
157
  ...state,
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "id": "pmo",
16
16
  "nickname": "pmo",
17
- "type": "claude",
17
+ "type": "auto",
18
18
  "role": "coordinate builders, track progress, enforce delivery cadence",
19
19
  "prompt_profile": "pmo-coordinator",
20
20
  "accept_from": [