@dotdrelle/wiki-manager 0.14.13 → 0.14.20
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/.env.example +29 -4
- package/README.md +19 -0
- package/docker-compose.yml +6 -1
- package/package.json +1 -1
- package/src/activity/activityAggregator.js +50 -16
- package/src/activity/activityAggregator.test.js +67 -4
- package/src/agent/graph.js +79 -11
- package/src/agent/graph.test.js +43 -3
- package/src/cli/wiki-manager.js +74 -11
- package/src/cli/wiki-manager.test.js +40 -1
- package/src/commands/slash.js +10 -3
- package/src/commands/slash.test.js +24 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +4 -0
- package/src/core/env.test.js +3 -0
- package/src/core/mcp.js +1 -1
- package/src/core/wikiSetup.js +35 -0
- package/src/core/wikiWorkspace.test.js +20 -0
- package/src/core/workflow.js +72 -0
- package/src/core/workflow.test.js +57 -0
- package/src/core/workspaces.js +10 -2
- package/src/orchestrator/dependencyResolver.js +19 -1
- package/src/orchestrator/objectiveResolver.js +24 -0
- package/src/orchestrator/objectiveResolver.test.js +23 -1
- package/src/orchestrator/scheduler.js +27 -7
- package/src/orchestrator/scheduler.test.js +45 -1
- package/src/runtime/auth.test.js +65 -1
- package/src/runtime/client.js +4 -0
- package/src/runtime/donna-contract.test.js +2 -0
- package/src/runtime/lifecycle.js +21 -12
- package/src/runtime/runner.js +141 -18
- package/src/runtime/runner.test.js +30 -0
- package/src/runtime/server.js +13 -2
- package/src/runtime/server.test.js +30 -1
- package/src/runtime/store.js +54 -0
- package/src/runtime/store.test.js +42 -0
- package/src/shell/FileEditorDialog.tsx +2 -2
- package/src/shell/LeftPane.tsx +60 -15
- package/src/shell/RightPane.tsx +168 -56
- package/src/shell/StartupScreen.tsx +3 -7
- package/src/shell/renderer.ts +1 -0
- package/src/shell/repl.js +7 -81
- package/src/shell/repl.test.js +141 -38
- package/src/shell/tui.tsx +65 -65
- package/src/shell/useSession.ts +92 -6
- package/wiki-workspace +28 -0
|
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
4
4
|
import { createInteractiveSession, ensureInteractiveAssistantMessage } from '../cli/wiki-manager.js';
|
|
5
|
-
import { runtimeState, startRuntimeServer as startRuntimeServerImpl } from './server.js';
|
|
5
|
+
import { approvalRequestFromStatus, runtimeState, startRuntimeServer as startRuntimeServerImpl } from './server.js';
|
|
6
6
|
|
|
7
7
|
// Most server tests exercise endpoint behavior rather than authentication. Keep
|
|
8
8
|
// them independent from a developer's WIKI_MANAGER_RUNTIME_TOKEN environment;
|
|
@@ -11,6 +11,35 @@ function startRuntimeServer(options) {
|
|
|
11
11
|
return startRuntimeServerImpl({ token: '', ...options });
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
test('approval fallback derives classes from waiting tasks when the approval queue is missing', () => {
|
|
15
|
+
const request = approvalRequestFromStatus({
|
|
16
|
+
workspace: 'acme',
|
|
17
|
+
runId: 'run-1',
|
|
18
|
+
planRevision: 4,
|
|
19
|
+
approvals: [],
|
|
20
|
+
plan: [
|
|
21
|
+
{ id: 'apply', status: 'waiting_approval', requiresApproval: true, approvalClass: 'mutation' },
|
|
22
|
+
{ id: 'export', status: 'waiting_approval', requiresApproval: true, approvalClass: 'external-write' },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(request.approvalClasses, ['mutation', 'external-write']);
|
|
27
|
+
assert.equal(request.scope, 'run');
|
|
28
|
+
assert.equal(request.planRevision, 4);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('approval fallback excludes tasks blocked by failed dependencies', () => {
|
|
32
|
+
const request = approvalRequestFromStatus({
|
|
33
|
+
workspace: 'acme', runId: 'run-1', planRevision: 4, approvals: [],
|
|
34
|
+
plan: [
|
|
35
|
+
{ id: 'failed-source', status: 'failed' },
|
|
36
|
+
{ id: 'blocked-write', status: 'waiting_approval', requiresApproval: true, approvalClass: 'external-write', dependsOn: ['failed-source'] },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.deepEqual(request.approvalClasses, ['default']);
|
|
41
|
+
});
|
|
42
|
+
|
|
14
43
|
test('interactive runtime sessions isolate canonical run state', () => {
|
|
15
44
|
const mcp = { wiki: { status: 'connected' } };
|
|
16
45
|
const session = createInteractiveSession({ session: {
|
package/src/runtime/store.js
CHANGED
|
@@ -541,6 +541,12 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
541
541
|
const listApprovalGrantsByRunStatement = db.prepare(`
|
|
542
542
|
SELECT * FROM approval_grants WHERE run_id = ? ORDER BY created_at ASC, id ASC
|
|
543
543
|
`);
|
|
544
|
+
const listPendingApprovalGrantsByRunStatement = db.prepare(`
|
|
545
|
+
SELECT * FROM approval_grants WHERE run_id = ? AND status = 'pending_approval'
|
|
546
|
+
`);
|
|
547
|
+
const markApprovalGrantApprovedStatement = db.prepare(`
|
|
548
|
+
UPDATE approval_grants SET status = 'approved', granted_at = ? WHERE id = ?
|
|
549
|
+
`);
|
|
544
550
|
|
|
545
551
|
function persistEvent(event) {
|
|
546
552
|
if (NON_PERSISTED_EVENT_TYPES.has(event.type)) return event;
|
|
@@ -687,10 +693,12 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
687
693
|
const delResults = db.prepare('DELETE FROM task_results WHERE task_id IN (SELECT id FROM tasks WHERE run_id = ?)');
|
|
688
694
|
const delAssignments = db.prepare('DELETE FROM task_assignments WHERE task_id IN (SELECT id FROM tasks WHERE run_id = ?)');
|
|
689
695
|
const delAttempts = db.prepare('DELETE FROM task_attempts WHERE run_id = ?');
|
|
696
|
+
const delApprovals = db.prepare('DELETE FROM approval_grants WHERE run_id = ?');
|
|
690
697
|
for (const runId of runIds) {
|
|
691
698
|
delResults.run(runId);
|
|
692
699
|
delAssignments.run(runId);
|
|
693
700
|
delAttempts.run(runId);
|
|
701
|
+
delApprovals.run(runId);
|
|
694
702
|
}
|
|
695
703
|
const events = (workspace
|
|
696
704
|
? db.prepare('DELETE FROM events WHERE workspace = ?').run(workspace)
|
|
@@ -1020,6 +1028,29 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
1020
1028
|
status === 'approved' ? (payload.grantedAt ?? event.ts) : null,
|
|
1021
1029
|
status === 'rejected' ? (payload.rejectedAt ?? event.ts) : null,
|
|
1022
1030
|
);
|
|
1031
|
+
// Mirror the in-memory projection's markCoveredApprovalsApproved into the
|
|
1032
|
+
// persisted table: a broad (run/group) grant covers the per-task
|
|
1033
|
+
// pending_approval requests, so mark them approved here too. Without this,
|
|
1034
|
+
// the scheduler proceeds (the projection knows they're covered) but the
|
|
1035
|
+
// approval_grants table keeps them pending_approval forever — orphaned rows
|
|
1036
|
+
// that pile up (32 per run observed) and survive run purges.
|
|
1037
|
+
if (status === 'approved') {
|
|
1038
|
+
const grantedAt = payload.grantedAt ?? event.ts;
|
|
1039
|
+
const grant = {
|
|
1040
|
+
scope: String(payload.scope ?? 'run'),
|
|
1041
|
+
runId: String(runId),
|
|
1042
|
+
workspaceId: event.workspace ?? payload.workspaceId ?? payload.workspace ?? null,
|
|
1043
|
+
planRevision: payload.planRevision == null ? null : Number(payload.planRevision),
|
|
1044
|
+
groupId: payload.groupId ?? null,
|
|
1045
|
+
approvalClasses: normalizeApprovalClasses(payload.approvalClasses ?? payload.approvalClass),
|
|
1046
|
+
ids: new Set([payload.taskId, payload.itemId, payload.approvalId, payload.id, event.taskId]
|
|
1047
|
+
.filter(Boolean).map(String)),
|
|
1048
|
+
};
|
|
1049
|
+
for (const row of listPendingApprovalGrantsByRunStatement.all(String(runId))) {
|
|
1050
|
+
if (String(row.id) === String(id)) continue;
|
|
1051
|
+
if (persistedGrantCovers(grant, row)) markApprovalGrantApprovedStatement.run(grantedAt, row.id);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1023
1054
|
}
|
|
1024
1055
|
|
|
1025
1056
|
function lifecycleTaskId(event) {
|
|
@@ -1177,6 +1208,10 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
1177
1208
|
return {
|
|
1178
1209
|
...baseState,
|
|
1179
1210
|
runs,
|
|
1211
|
+
// Resolved scheduler concurrency for the live run (display-only). Null for
|
|
1212
|
+
// replayed/historical state — the UIs then fall back to the plan-derived
|
|
1213
|
+
// value. Never consumed by scheduling.
|
|
1214
|
+
concurrency: session?._runConcurrency ?? null,
|
|
1180
1215
|
workflow: projectWorkflow({ ...baseState, runs, workspace }, events ?? session?.agentEvents ?? []),
|
|
1181
1216
|
};
|
|
1182
1217
|
}
|
|
@@ -1281,9 +1316,11 @@ function purgeOldTerminalRuns(db, now = new Date()) {
|
|
|
1281
1316
|
db.exec('BEGIN');
|
|
1282
1317
|
try {
|
|
1283
1318
|
const deleteEvents = db.prepare('DELETE FROM events WHERE run_id = ? OR json_extract(payload, \'$.runId\') = ?');
|
|
1319
|
+
const deleteApprovals = db.prepare('DELETE FROM approval_grants WHERE run_id = ?');
|
|
1284
1320
|
const deleteRun = db.prepare('DELETE FROM runs WHERE id = ?');
|
|
1285
1321
|
for (const runId of oldRuns) {
|
|
1286
1322
|
deleteEvents.run(runId, runId);
|
|
1323
|
+
deleteApprovals.run(runId);
|
|
1287
1324
|
deleteRun.run(runId);
|
|
1288
1325
|
}
|
|
1289
1326
|
db.exec('COMMIT');
|
|
@@ -1403,6 +1440,23 @@ function normalizeApprovalClasses(value) {
|
|
|
1403
1440
|
return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))];
|
|
1404
1441
|
}
|
|
1405
1442
|
|
|
1443
|
+
// Persisted-table twin of markCoveredApprovalsApproved (core/agentEvents.js):
|
|
1444
|
+
// does `grant` (already-approved) cover the pending approval_grants `row`?
|
|
1445
|
+
// Kept in sync with that reducer so the DB never disagrees with the projection.
|
|
1446
|
+
function persistedGrantCovers(grant, row) {
|
|
1447
|
+
if (grant.runId != null && row.run_id != null && String(grant.runId) !== String(row.run_id)) return false;
|
|
1448
|
+
if (grant.workspaceId != null && row.workspace_id != null && String(grant.workspaceId) !== String(row.workspace_id)) return false;
|
|
1449
|
+
if (grant.planRevision != null && row.plan_revision != null && Number(grant.planRevision) !== Number(row.plan_revision)) return false;
|
|
1450
|
+
const rowClasses = row.approval_classes ? normalizeApprovalClasses(parseJsonMaybe(row.approval_classes)) : [];
|
|
1451
|
+
if (grant.approvalClasses.length > 0 && rowClasses.length > 0 && !rowClasses.some((item) => grant.approvalClasses.includes(item))) return false;
|
|
1452
|
+
if (grant.scope === 'group' && String(grant.groupId ?? '') !== String(row.group_id ?? '')) return false;
|
|
1453
|
+
if (grant.scope === 'task' || grant.scope === 'tool') {
|
|
1454
|
+
const rowIds = [row.task_id, row.id].filter(Boolean).map(String);
|
|
1455
|
+
if (!rowIds.some((id) => grant.ids.has(id))) return false;
|
|
1456
|
+
}
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1406
1460
|
function parseJsonMaybe(value) {
|
|
1407
1461
|
try {
|
|
1408
1462
|
return JSON.parse(value);
|
|
@@ -540,6 +540,48 @@ test('runtime store persists bounded approval grants', () => {
|
|
|
540
540
|
store.close();
|
|
541
541
|
});
|
|
542
542
|
|
|
543
|
+
test('runtime store: a run-scope grant marks covered task grants approved (no orphans)', () => {
|
|
544
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
545
|
+
const store = openRuntimeStore({ stateDir });
|
|
546
|
+
// Two per-task approval requests for the same run.
|
|
547
|
+
for (const taskId of ['run-x:ingest-a', 'run-x:ingest-b']) {
|
|
548
|
+
store.persistEvent(createAgentEvent('approval.requested', {
|
|
549
|
+
origin: 'runtime',
|
|
550
|
+
runId: 'run-x',
|
|
551
|
+
workspace: 'docs',
|
|
552
|
+
taskId,
|
|
553
|
+
payload: { id: `approval:${taskId}`, scope: 'task', runId: 'run-x', workspaceId: 'docs', taskId },
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
assert.equal(store.listApprovalGrants({ runId: 'run-x' }).filter((g) => g.status === 'pending_approval').length, 2);
|
|
557
|
+
|
|
558
|
+
// A single run-scope grant covers both.
|
|
559
|
+
store.persistEvent(createAgentEvent('approval.granted', {
|
|
560
|
+
origin: 'runtime',
|
|
561
|
+
runId: 'run-x',
|
|
562
|
+
workspace: 'docs',
|
|
563
|
+
payload: { id: 'grant-run-x', scope: 'run', runId: 'run-x', workspaceId: 'docs' },
|
|
564
|
+
}));
|
|
565
|
+
const grants = store.listApprovalGrants({ runId: 'run-x' });
|
|
566
|
+
assert.equal(grants.filter((g) => g.status === 'pending_approval').length, 0, 'no task grant left pending');
|
|
567
|
+
assert.equal(grants.filter((g) => g.status === 'approved').length, 3, 'both tasks + run grant approved');
|
|
568
|
+
store.close();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('runtime store: clearWorkspaceState removes approval grants', () => {
|
|
572
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
573
|
+
const store = openRuntimeStore({ stateDir });
|
|
574
|
+
store.persistEvent(createAgentEvent('run_started', { origin: 'runtime', runId: 'run-y', workspace: 'docs', payload: { runId: 'run-y' } }));
|
|
575
|
+
store.persistEvent(createAgentEvent('approval.requested', {
|
|
576
|
+
origin: 'runtime', runId: 'run-y', workspace: 'docs', taskId: 'run-y:t',
|
|
577
|
+
payload: { id: 'approval:run-y:t', scope: 'task', runId: 'run-y', workspaceId: 'docs', taskId: 'run-y:t' },
|
|
578
|
+
}));
|
|
579
|
+
assert.equal(store.listApprovalGrants({ runId: 'run-y' }).length, 1);
|
|
580
|
+
store.clearWorkspaceState({ workspace: 'docs' });
|
|
581
|
+
assert.equal(store.listApprovalGrants({ runId: 'run-y' }).length, 0);
|
|
582
|
+
store.close();
|
|
583
|
+
});
|
|
584
|
+
|
|
543
585
|
test('runtime store orders events by durable sequence', () => {
|
|
544
586
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
545
587
|
const store = openRuntimeStore({ stateDir });
|
|
@@ -83,9 +83,9 @@ export function FileEditorDialog(props: {
|
|
|
83
83
|
{(message) => <text height={1} fg="#F87171">{message()}</text>}
|
|
84
84
|
</Show>
|
|
85
85
|
<box height={1} flexDirection="row">
|
|
86
|
-
<text fg="#111318" bg="#8BD5CA"> Save Ctrl+S </text>
|
|
86
|
+
<text fg="#111318" bg="#8BD5CA" onMouseUp={save}> Save Ctrl+S </text>
|
|
87
87
|
<text fg="#7F8C8D"> </text>
|
|
88
|
-
<text fg="#D6DEE8" bg="#4B5563"> Cancel Esc </text>
|
|
88
|
+
<text fg="#D6DEE8" bg="#4B5563" onMouseUp={props.onCancel}> Cancel Esc </text>
|
|
89
89
|
</box>
|
|
90
90
|
</box>
|
|
91
91
|
)}
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { For, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
3
|
+
import { StyledText, bg as styledBg, fg as styledFg, link as styledLink } from '@opentui/core';
|
|
3
4
|
import { colorForRenderedLine, helpCommandParts, keyValueParts, renderPlainMarkdown } from './renderer';
|
|
4
5
|
import { httpLinkParts, wrapHttpLinks } from './externalLinks.js';
|
|
5
6
|
|
|
@@ -49,8 +50,20 @@ function wrapLine(line: string, width: number) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
type Segment = { text: string; color: string; width?: number; bg?: string; url?: string };
|
|
53
|
+
|
|
54
|
+
function styledSegments(segments: Segment[]) {
|
|
55
|
+
return new StyledText(segments.map((segment) => {
|
|
56
|
+
const text = segment.width ? segment.text.padEnd(segment.width) : segment.text;
|
|
57
|
+
let chunk = styledFg(segment.color)(text);
|
|
58
|
+
if (segment.bg) chunk = styledBg(segment.bg)(chunk);
|
|
59
|
+
if (segment.url) chunk = styledLink(segment.url)(chunk);
|
|
60
|
+
return chunk;
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
52
63
|
type RenderedLine = {
|
|
53
64
|
segments: Segment[];
|
|
65
|
+
role?: string;
|
|
66
|
+
userBubbleWidth?: number;
|
|
54
67
|
status?: boolean;
|
|
55
68
|
statusLeft?: string;
|
|
56
69
|
statusRight?: string;
|
|
@@ -376,19 +389,35 @@ function statusColumns(line: string): { left: string; right: string } {
|
|
|
376
389
|
return { left: left || ' ', right };
|
|
377
390
|
}
|
|
378
391
|
|
|
392
|
+
// Consecutive messages that render under the same header ([shell]/[user]/
|
|
393
|
+
// [donna]) are grouped: only the first one shows the label + separator, so a
|
|
394
|
+
// run of same-type messages doesn't repeat the header on every entry.
|
|
395
|
+
function headerGroupKey(role: string) {
|
|
396
|
+
return isDonnaRole(role) ? 'donna' : roleLabel(role);
|
|
397
|
+
}
|
|
398
|
+
|
|
379
399
|
function conversationLines(messages: Array<{ role: string; content: string }>, columns: number): RenderedLine[] {
|
|
380
|
-
return messages.flatMap((message) => {
|
|
400
|
+
return messages.flatMap((message, index) => {
|
|
381
401
|
const raw = String(message.content || '');
|
|
402
|
+
const previous = index > 0 ? messages[index - 1] : null;
|
|
403
|
+
const showHeader = !previous || headerGroupKey(previous.role) !== headerGroupKey(message.role);
|
|
404
|
+
const headerLines: RenderedLine[] = showHeader
|
|
405
|
+
? [
|
|
406
|
+
{ segments: messageHeaderSegments(message.role, columns), copyContent: raw },
|
|
407
|
+
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
408
|
+
]
|
|
409
|
+
: [];
|
|
410
|
+
const contentColumns = message.role === 'user' ? Math.max(12, Math.floor(columns * 0.6) - 2) : columns;
|
|
382
411
|
if (isStatusOutput(message)) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
412
|
+
const statusLines: RenderedLine[] = [
|
|
413
|
+
...headerLines,
|
|
386
414
|
...raw.split('\n').map((line) => {
|
|
387
415
|
const { left, right } = statusColumns(line || ' ');
|
|
388
416
|
return { status: true, statusLeft: left, statusRight: right, segments: [] };
|
|
389
417
|
}),
|
|
390
418
|
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
391
419
|
];
|
|
420
|
+
return statusLines.map((line) => ({ ...line, role: message.role }));
|
|
392
421
|
}
|
|
393
422
|
let inFence = false;
|
|
394
423
|
const lines: Array<{ text: string; isCode: boolean }> = [];
|
|
@@ -399,12 +428,19 @@ function conversationLines(messages: Array<{ role: string; content: string }>, c
|
|
|
399
428
|
if (/^\s*(`{3,}|~{3,})/.test(line)) { inFence = !inFence; continue; }
|
|
400
429
|
lines.push({ text: line, isCode: inFence });
|
|
401
430
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
431
|
+
const renderedBody = renderMarkdownLines(lines, message.role, contentColumns);
|
|
432
|
+
const userBubbleWidth = message.role === 'user'
|
|
433
|
+
? Math.min(
|
|
434
|
+
Math.max(1, ...renderedBody.map((line) => line.segments.reduce((sum, segment) => sum + segment.text.length, 0))) + 2,
|
|
435
|
+
Math.max(12, Math.floor(columns * 0.6)),
|
|
436
|
+
)
|
|
437
|
+
: undefined;
|
|
438
|
+
const bodyLines: RenderedLine[] = [
|
|
439
|
+
...headerLines,
|
|
440
|
+
...renderedBody.map((line) => ({ ...line, userBubbleWidth })),
|
|
406
441
|
{ segments: [{ text: ' ', color: '#D6DEE8' }] },
|
|
407
442
|
];
|
|
443
|
+
return bodyLines.map((line) => ({ ...line, role: message.role }));
|
|
408
444
|
});
|
|
409
445
|
}
|
|
410
446
|
|
|
@@ -462,6 +498,21 @@ export function ConversationView(props: {
|
|
|
462
498
|
</For>
|
|
463
499
|
<text fg="#4B5563" content={COPY_BTN} onMouseUp={() => props.onCopy?.(line.copyContent!)} />
|
|
464
500
|
</box>
|
|
501
|
+
) : line.role === 'user' && line.userBubbleWidth ? (
|
|
502
|
+
<box height={1} flexDirection="row" overflow="hidden">
|
|
503
|
+
<box flexGrow={1} />
|
|
504
|
+
<box
|
|
505
|
+
width={line.userBubbleWidth}
|
|
506
|
+
height={1}
|
|
507
|
+
flexDirection="row"
|
|
508
|
+
flexShrink={1}
|
|
509
|
+
justifyContent="flex-start"
|
|
510
|
+
paddingX={1}
|
|
511
|
+
overflow="hidden"
|
|
512
|
+
>
|
|
513
|
+
<text>{styledSegments(line.segments)}</text>
|
|
514
|
+
</box>
|
|
515
|
+
</box>
|
|
465
516
|
) : line.status ? (
|
|
466
517
|
<box height={1} flexDirection="row" gap={2} overflow="hidden">
|
|
467
518
|
<box
|
|
@@ -499,13 +550,7 @@ export function ConversationView(props: {
|
|
|
499
550
|
</box>
|
|
500
551
|
) : (
|
|
501
552
|
<box height={1} flexDirection="row" overflow="hidden">
|
|
502
|
-
<
|
|
503
|
-
{(seg: Segment) => (
|
|
504
|
-
<text width={seg.width} fg={seg.color} bg={seg.bg}>
|
|
505
|
-
{seg.url ? <a href={seg.url}>{seg.text}</a> : seg.text}
|
|
506
|
-
</text>
|
|
507
|
-
)}
|
|
508
|
-
</For>
|
|
553
|
+
<text>{styledSegments(line.segments)}</text>
|
|
509
554
|
</box>
|
|
510
555
|
)
|
|
511
556
|
)}
|