memorix 1.1.7 → 1.1.8
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/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
package/src/cli/tui/ChatView.tsx
CHANGED
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
|
|
2
|
-
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
-
import { ScrollView, type ScrollViewRef } from 'ink-scroll-view';
|
|
4
|
-
import Spinner from 'ink-spinner';
|
|
5
|
-
import { COLORS, SEP, SYMBOLS, computeLayoutWidths } from './theme.js';
|
|
6
|
-
import type { ProjectInfo } from './data.js';
|
|
7
|
-
import type { ChatSource } from './chat-service.js';
|
|
8
|
-
|
|
9
|
-
export interface ChatTranscriptMessage {
|
|
10
|
-
role: 'user' | 'assistant';
|
|
11
|
-
content: string;
|
|
12
|
-
sources?: ChatSource[];
|
|
13
|
-
error?: boolean;
|
|
14
|
-
timestamp?: string;
|
|
15
|
-
meta?: {
|
|
16
|
-
usedLLM?: boolean;
|
|
17
|
-
searchMode?: string;
|
|
18
|
-
llmModel?: string;
|
|
19
|
-
warning?: string;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ChatViewProps {
|
|
24
|
-
project: ProjectInfo | null;
|
|
25
|
-
messages: ChatTranscriptMessage[];
|
|
26
|
-
loading: boolean;
|
|
27
|
-
contentWidth?: number;
|
|
28
|
-
viewportHeight?: number;
|
|
29
|
-
threadId?: string;
|
|
30
|
-
keyboardScrollEnabled?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function separator(width: number): string {
|
|
34
|
-
return SEP.thin.repeat(Math.max(12, Math.min(width, 72)));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function truncate(text: string, max: number): string {
|
|
38
|
-
if (text.length <= max) return text;
|
|
39
|
-
return `${text.slice(0, max - 1)}…`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Measure the display width of a string, counting CJK characters as 2 columns. */
|
|
43
|
-
function displayWidth(str: string): number {
|
|
44
|
-
let w = 0;
|
|
45
|
-
for (const ch of str) {
|
|
46
|
-
const cp = ch.codePointAt(0)!;
|
|
47
|
-
// CJK Unified Ideographs + CJK Extension + Katakana + Hangul + fullwidth forms
|
|
48
|
-
if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
|
|
49
|
-
(cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
50
|
-
(cp >= 0x3400 && cp <= 0x4DBF)) {
|
|
51
|
-
w += 2;
|
|
52
|
-
} else {
|
|
53
|
-
w += 1;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return w;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Split text into lines respecting CJK double-width characters.
|
|
60
|
-
* Ink's wrap="wrap" doesn't understand CJK width, causing garbled text. */
|
|
61
|
-
function cjkAwareLines(text: string, maxCols: number): string[] {
|
|
62
|
-
if (maxCols < 4) return [text];
|
|
63
|
-
const lines: string[] = [];
|
|
64
|
-
let line = '';
|
|
65
|
-
let lineW = 0;
|
|
66
|
-
for (const ch of text) {
|
|
67
|
-
if (ch === '\n') {
|
|
68
|
-
lines.push(line);
|
|
69
|
-
line = '';
|
|
70
|
-
lineW = 0;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const cw = displayWidth(ch);
|
|
74
|
-
if (lineW + cw > maxCols && line.length > 0) {
|
|
75
|
-
lines.push(line);
|
|
76
|
-
line = ch;
|
|
77
|
-
lineW = cw;
|
|
78
|
-
} else {
|
|
79
|
-
line += ch;
|
|
80
|
-
lineW += cw;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (line) lines.push(line);
|
|
84
|
-
return lines.length > 0 ? lines : [''];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function MessageBlock({ message, width }: { message: ChatTranscriptMessage; width: number }): React.ReactElement {
|
|
88
|
-
const heading = message.role === 'user' ? 'You' : 'Memorix';
|
|
89
|
-
const headingColor = message.role === 'user'
|
|
90
|
-
? COLORS.brand
|
|
91
|
-
: message.error
|
|
92
|
-
? COLORS.error
|
|
93
|
-
: COLORS.assistant;
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<Box flexDirection="column" marginBottom={0}>
|
|
97
|
-
<Box>
|
|
98
|
-
<Text color={headingColor} bold wrap="truncate-end">{heading}</Text>
|
|
99
|
-
{message.meta?.usedLLM && message.role === 'assistant' && (
|
|
100
|
-
<Text color={COLORS.muted} wrap="truncate-end"> {SYMBOLS.pill(message.meta.llmModel || 'LLM')}</Text>
|
|
101
|
-
)}
|
|
102
|
-
{message.meta?.searchMode && message.role === 'assistant' && (
|
|
103
|
-
<Text color={COLORS.textDim} wrap="truncate-end">{` ${SYMBOLS.pill(message.meta.searchMode)}`}</Text>
|
|
104
|
-
)}
|
|
105
|
-
</Box>
|
|
106
|
-
<Box flexDirection="column">
|
|
107
|
-
{cjkAwareLines(message.content, Math.max(20, width - 4)).map((ln, i) => (
|
|
108
|
-
<Text key={i} color={message.error ? COLORS.error : COLORS.text}>{ln}</Text>
|
|
109
|
-
))}
|
|
110
|
-
</Box>
|
|
111
|
-
{message.meta?.warning && !message.error && (
|
|
112
|
-
<Text color={COLORS.warning} wrap="truncate-end">{message.meta.warning}</Text>
|
|
113
|
-
)}
|
|
114
|
-
{message.sources && message.sources.length > 0 && (
|
|
115
|
-
<Box flexDirection="column">
|
|
116
|
-
<Text color={COLORS.muted} wrap="truncate-end">Sources</Text>
|
|
117
|
-
{message.sources.slice(0, 3).map((source) => (
|
|
118
|
-
<Text key={source.id} color={COLORS.textDim} wrap="truncate-end">
|
|
119
|
-
{` [obs:${source.id}] ${truncate(source.title, Math.max(20, width - 16))}`}
|
|
120
|
-
</Text>
|
|
121
|
-
))}
|
|
122
|
-
</Box>
|
|
123
|
-
)}
|
|
124
|
-
</Box>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface ChatViewRef {
|
|
129
|
-
scrollBy: (offset: number) => void;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const ChatView = forwardRef<ChatViewRef, ChatViewProps>(function ChatView(
|
|
133
|
-
{ project, messages, loading, contentWidth: _contentWidth, viewportHeight, threadId, keyboardScrollEnabled = true },
|
|
134
|
-
ref,
|
|
135
|
-
): React.ReactElement {
|
|
136
|
-
const scrollRef = useRef<ScrollViewRef>(null);
|
|
137
|
-
const { stdout } = useStdout();
|
|
138
|
-
|
|
139
|
-
// Compute responsive content width from terminal size (shared with App.tsx)
|
|
140
|
-
const termWidth = stdout?.columns || 80;
|
|
141
|
-
const { contentWidth } = computeLayoutWidths(termWidth);
|
|
142
|
-
|
|
143
|
-
// Expose scrollBy to parent for centralized mouse handling
|
|
144
|
-
useImperativeHandle(ref, () => ({
|
|
145
|
-
scrollBy: (offset: number) => scrollRef.current?.scrollBy(offset),
|
|
146
|
-
}), []);
|
|
147
|
-
|
|
148
|
-
// Auto-scroll to bottom when new messages arrive
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (scrollRef.current && messages.length > 0) {
|
|
151
|
-
// Small delay to let ink-scroll-view measure new content
|
|
152
|
-
const timer = setTimeout(() => {
|
|
153
|
-
scrollRef.current?.remeasure();
|
|
154
|
-
// Scroll to bottom: use getBottomOffset
|
|
155
|
-
const bottom = scrollRef.current?.getBottomOffset?.() ?? 0;
|
|
156
|
-
if (bottom > 0) {
|
|
157
|
-
scrollRef.current?.scrollTo?.(bottom);
|
|
158
|
-
}
|
|
159
|
-
}, 50);
|
|
160
|
-
return () => clearTimeout(timer);
|
|
161
|
-
}
|
|
162
|
-
}, [messages.length, loading]);
|
|
163
|
-
|
|
164
|
-
// Handle terminal resize
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
const handleResize = () => scrollRef.current?.remeasure();
|
|
167
|
-
stdout?.on('resize', handleResize);
|
|
168
|
-
return () => { stdout?.off('resize', handleResize); };
|
|
169
|
-
}, [stdout]);
|
|
170
|
-
|
|
171
|
-
// Capture up/down keys for scrolling when in chat view
|
|
172
|
-
useInput(useCallback((_ch: string, key: any) => {
|
|
173
|
-
if (!scrollRef.current) return;
|
|
174
|
-
if (key.upArrow) {
|
|
175
|
-
scrollRef.current.scrollBy(-3);
|
|
176
|
-
} else if (key.downArrow) {
|
|
177
|
-
scrollRef.current.scrollBy(3);
|
|
178
|
-
} else if (key.pageUp) {
|
|
179
|
-
const vh = scrollRef.current.getViewportHeight?.() ?? 10;
|
|
180
|
-
scrollRef.current.scrollBy(-(vh - 2));
|
|
181
|
-
} else if (key.pageDown) {
|
|
182
|
-
const vh = scrollRef.current.getViewportHeight?.() ?? 10;
|
|
183
|
-
scrollRef.current.scrollBy(vh - 2);
|
|
184
|
-
}
|
|
185
|
-
}, []), { isActive: keyboardScrollEnabled });
|
|
186
|
-
|
|
187
|
-
// Reserve 2 rows for header ("Conversation" + separator), 1 for loading
|
|
188
|
-
const headerRows = 2;
|
|
189
|
-
const loadingRows = loading ? 1 : 0;
|
|
190
|
-
const scrollHeight = viewportHeight ? Math.max(4, viewportHeight - headerRows - loadingRows) : 20;
|
|
191
|
-
|
|
192
|
-
const threadLabel = threadId && threadId !== 'default' ? ` (${threadId})` : '';
|
|
193
|
-
|
|
194
|
-
if (messages.length === 0) {
|
|
195
|
-
return (
|
|
196
|
-
<Box flexDirection="column" paddingX={1}>
|
|
197
|
-
<Text color={COLORS.brand} bold>Memory Chat{threadLabel}</Text>
|
|
198
|
-
<Text color={COLORS.border}>{separator(contentWidth - 4)}</Text>
|
|
199
|
-
<Text color={COLORS.text}>Ask Memorix about project decisions, bugs, rationale, or recent changes.</Text>
|
|
200
|
-
<Text color={COLORS.textDim}>{project ? `Project: ${project.name}` : 'Open inside a git project to query its memory graph.'}</Text>
|
|
201
|
-
<Box marginTop={1} flexDirection="column">
|
|
202
|
-
<Text color={COLORS.muted}>Try asking</Text>
|
|
203
|
-
<Text color={COLORS.textDim}> {SYMBOLS.arrow} Why did we choose this architecture?</Text>
|
|
204
|
-
<Text color={COLORS.textDim}> {SYMBOLS.arrow} What changed in auth recently?</Text>
|
|
205
|
-
<Text color={COLORS.textDim}> {SYMBOLS.arrow} Summarize known issues around embeddings</Text>
|
|
206
|
-
<Text color={COLORS.textDim}> {SYMBOLS.arrow} /search timeout bug</Text>
|
|
207
|
-
</Box>
|
|
208
|
-
</Box>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return (
|
|
213
|
-
<Box flexDirection="column" paddingX={1}>
|
|
214
|
-
<Text color={COLORS.brand} bold>Conversation{threadLabel}</Text>
|
|
215
|
-
<Text color={COLORS.border}>{separator(contentWidth - 4)}</Text>
|
|
216
|
-
<Box height={scrollHeight} flexDirection="column">
|
|
217
|
-
<ScrollView ref={scrollRef}>
|
|
218
|
-
{messages.map((message, index) => (
|
|
219
|
-
<MessageBlock
|
|
220
|
-
key={`${message.role}-${index}-${message.timestamp || index}`}
|
|
221
|
-
message={message}
|
|
222
|
-
width={contentWidth}
|
|
223
|
-
/>
|
|
224
|
-
))}
|
|
225
|
-
{loading && (
|
|
226
|
-
<Box>
|
|
227
|
-
<Text color={COLORS.brand}><Spinner type="dots" />{' '}Thinking…</Text>
|
|
228
|
-
</Box>
|
|
229
|
-
)}
|
|
230
|
-
</ScrollView>
|
|
231
|
-
</Box>
|
|
232
|
-
</Box>
|
|
233
|
-
);
|
|
234
|
-
});
|
|
1
|
+
import React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
+
import { ScrollView, type ScrollViewRef } from 'ink-scroll-view';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { COLORS, SEP, SYMBOLS, computeLayoutWidths } from './theme.js';
|
|
6
|
+
import type { ProjectInfo } from './data.js';
|
|
7
|
+
import type { ChatSource } from './chat-service.js';
|
|
8
|
+
|
|
9
|
+
export interface ChatTranscriptMessage {
|
|
10
|
+
role: 'user' | 'assistant';
|
|
11
|
+
content: string;
|
|
12
|
+
sources?: ChatSource[];
|
|
13
|
+
error?: boolean;
|
|
14
|
+
timestamp?: string;
|
|
15
|
+
meta?: {
|
|
16
|
+
usedLLM?: boolean;
|
|
17
|
+
searchMode?: string;
|
|
18
|
+
llmModel?: string;
|
|
19
|
+
warning?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChatViewProps {
|
|
24
|
+
project: ProjectInfo | null;
|
|
25
|
+
messages: ChatTranscriptMessage[];
|
|
26
|
+
loading: boolean;
|
|
27
|
+
contentWidth?: number;
|
|
28
|
+
viewportHeight?: number;
|
|
29
|
+
threadId?: string;
|
|
30
|
+
keyboardScrollEnabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function separator(width: number): string {
|
|
34
|
+
return SEP.thin.repeat(Math.max(12, Math.min(width, 72)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function truncate(text: string, max: number): string {
|
|
38
|
+
if (text.length <= max) return text;
|
|
39
|
+
return `${text.slice(0, max - 1)}…`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Measure the display width of a string, counting CJK characters as 2 columns. */
|
|
43
|
+
function displayWidth(str: string): number {
|
|
44
|
+
let w = 0;
|
|
45
|
+
for (const ch of str) {
|
|
46
|
+
const cp = ch.codePointAt(0)!;
|
|
47
|
+
// CJK Unified Ideographs + CJK Extension + Katakana + Hangul + fullwidth forms
|
|
48
|
+
if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
|
|
49
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
50
|
+
(cp >= 0x3400 && cp <= 0x4DBF)) {
|
|
51
|
+
w += 2;
|
|
52
|
+
} else {
|
|
53
|
+
w += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return w;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Split text into lines respecting CJK double-width characters.
|
|
60
|
+
* Ink's wrap="wrap" doesn't understand CJK width, causing garbled text. */
|
|
61
|
+
function cjkAwareLines(text: string, maxCols: number): string[] {
|
|
62
|
+
if (maxCols < 4) return [text];
|
|
63
|
+
const lines: string[] = [];
|
|
64
|
+
let line = '';
|
|
65
|
+
let lineW = 0;
|
|
66
|
+
for (const ch of text) {
|
|
67
|
+
if (ch === '\n') {
|
|
68
|
+
lines.push(line);
|
|
69
|
+
line = '';
|
|
70
|
+
lineW = 0;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const cw = displayWidth(ch);
|
|
74
|
+
if (lineW + cw > maxCols && line.length > 0) {
|
|
75
|
+
lines.push(line);
|
|
76
|
+
line = ch;
|
|
77
|
+
lineW = cw;
|
|
78
|
+
} else {
|
|
79
|
+
line += ch;
|
|
80
|
+
lineW += cw;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (line) lines.push(line);
|
|
84
|
+
return lines.length > 0 ? lines : [''];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function MessageBlock({ message, width }: { message: ChatTranscriptMessage; width: number }): React.ReactElement {
|
|
88
|
+
const heading = message.role === 'user' ? 'You' : 'Memorix';
|
|
89
|
+
const headingColor = message.role === 'user'
|
|
90
|
+
? COLORS.brand
|
|
91
|
+
: message.error
|
|
92
|
+
? COLORS.error
|
|
93
|
+
: COLORS.assistant;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Box flexDirection="column" marginBottom={0}>
|
|
97
|
+
<Box>
|
|
98
|
+
<Text color={headingColor} bold wrap="truncate-end">{heading}</Text>
|
|
99
|
+
{message.meta?.usedLLM && message.role === 'assistant' && (
|
|
100
|
+
<Text color={COLORS.muted} wrap="truncate-end"> {SYMBOLS.pill(message.meta.llmModel || 'LLM')}</Text>
|
|
101
|
+
)}
|
|
102
|
+
{message.meta?.searchMode && message.role === 'assistant' && (
|
|
103
|
+
<Text color={COLORS.textDim} wrap="truncate-end">{` ${SYMBOLS.pill(message.meta.searchMode)}`}</Text>
|
|
104
|
+
)}
|
|
105
|
+
</Box>
|
|
106
|
+
<Box flexDirection="column">
|
|
107
|
+
{cjkAwareLines(message.content, Math.max(20, width - 4)).map((ln, i) => (
|
|
108
|
+
<Text key={i} color={message.error ? COLORS.error : COLORS.text}>{ln}</Text>
|
|
109
|
+
))}
|
|
110
|
+
</Box>
|
|
111
|
+
{message.meta?.warning && !message.error && (
|
|
112
|
+
<Text color={COLORS.warning} wrap="truncate-end">{message.meta.warning}</Text>
|
|
113
|
+
)}
|
|
114
|
+
{message.sources && message.sources.length > 0 && (
|
|
115
|
+
<Box flexDirection="column">
|
|
116
|
+
<Text color={COLORS.muted} wrap="truncate-end">Sources</Text>
|
|
117
|
+
{message.sources.slice(0, 3).map((source) => (
|
|
118
|
+
<Text key={source.id} color={COLORS.textDim} wrap="truncate-end">
|
|
119
|
+
{` [obs:${source.id}] ${truncate(source.title, Math.max(20, width - 16))}`}
|
|
120
|
+
</Text>
|
|
121
|
+
))}
|
|
122
|
+
</Box>
|
|
123
|
+
)}
|
|
124
|
+
</Box>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ChatViewRef {
|
|
129
|
+
scrollBy: (offset: number) => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const ChatView = forwardRef<ChatViewRef, ChatViewProps>(function ChatView(
|
|
133
|
+
{ project, messages, loading, contentWidth: _contentWidth, viewportHeight, threadId, keyboardScrollEnabled = true },
|
|
134
|
+
ref,
|
|
135
|
+
): React.ReactElement {
|
|
136
|
+
const scrollRef = useRef<ScrollViewRef>(null);
|
|
137
|
+
const { stdout } = useStdout();
|
|
138
|
+
|
|
139
|
+
// Compute responsive content width from terminal size (shared with App.tsx)
|
|
140
|
+
const termWidth = stdout?.columns || 80;
|
|
141
|
+
const { contentWidth } = computeLayoutWidths(termWidth);
|
|
142
|
+
|
|
143
|
+
// Expose scrollBy to parent for centralized mouse handling
|
|
144
|
+
useImperativeHandle(ref, () => ({
|
|
145
|
+
scrollBy: (offset: number) => scrollRef.current?.scrollBy(offset),
|
|
146
|
+
}), []);
|
|
147
|
+
|
|
148
|
+
// Auto-scroll to bottom when new messages arrive
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (scrollRef.current && messages.length > 0) {
|
|
151
|
+
// Small delay to let ink-scroll-view measure new content
|
|
152
|
+
const timer = setTimeout(() => {
|
|
153
|
+
scrollRef.current?.remeasure();
|
|
154
|
+
// Scroll to bottom: use getBottomOffset
|
|
155
|
+
const bottom = scrollRef.current?.getBottomOffset?.() ?? 0;
|
|
156
|
+
if (bottom > 0) {
|
|
157
|
+
scrollRef.current?.scrollTo?.(bottom);
|
|
158
|
+
}
|
|
159
|
+
}, 50);
|
|
160
|
+
return () => clearTimeout(timer);
|
|
161
|
+
}
|
|
162
|
+
}, [messages.length, loading]);
|
|
163
|
+
|
|
164
|
+
// Handle terminal resize
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
const handleResize = () => scrollRef.current?.remeasure();
|
|
167
|
+
stdout?.on('resize', handleResize);
|
|
168
|
+
return () => { stdout?.off('resize', handleResize); };
|
|
169
|
+
}, [stdout]);
|
|
170
|
+
|
|
171
|
+
// Capture up/down keys for scrolling when in chat view
|
|
172
|
+
useInput(useCallback((_ch: string, key: any) => {
|
|
173
|
+
if (!scrollRef.current) return;
|
|
174
|
+
if (key.upArrow) {
|
|
175
|
+
scrollRef.current.scrollBy(-3);
|
|
176
|
+
} else if (key.downArrow) {
|
|
177
|
+
scrollRef.current.scrollBy(3);
|
|
178
|
+
} else if (key.pageUp) {
|
|
179
|
+
const vh = scrollRef.current.getViewportHeight?.() ?? 10;
|
|
180
|
+
scrollRef.current.scrollBy(-(vh - 2));
|
|
181
|
+
} else if (key.pageDown) {
|
|
182
|
+
const vh = scrollRef.current.getViewportHeight?.() ?? 10;
|
|
183
|
+
scrollRef.current.scrollBy(vh - 2);
|
|
184
|
+
}
|
|
185
|
+
}, []), { isActive: keyboardScrollEnabled });
|
|
186
|
+
|
|
187
|
+
// Reserve 2 rows for header ("Conversation" + separator), 1 for loading
|
|
188
|
+
const headerRows = 2;
|
|
189
|
+
const loadingRows = loading ? 1 : 0;
|
|
190
|
+
const scrollHeight = viewportHeight ? Math.max(4, viewportHeight - headerRows - loadingRows) : 20;
|
|
191
|
+
|
|
192
|
+
const threadLabel = threadId && threadId !== 'default' ? ` (${threadId})` : '';
|
|
193
|
+
|
|
194
|
+
if (messages.length === 0) {
|
|
195
|
+
return (
|
|
196
|
+
<Box flexDirection="column" paddingX={1}>
|
|
197
|
+
<Text color={COLORS.brand} bold>Memory Chat{threadLabel}</Text>
|
|
198
|
+
<Text color={COLORS.border}>{separator(contentWidth - 4)}</Text>
|
|
199
|
+
<Text color={COLORS.text}>Ask Memorix about project decisions, bugs, rationale, or recent changes.</Text>
|
|
200
|
+
<Text color={COLORS.textDim}>{project ? `Project: ${project.name}` : 'Open inside a git project to query its memory graph.'}</Text>
|
|
201
|
+
<Box marginTop={1} flexDirection="column">
|
|
202
|
+
<Text color={COLORS.muted}>Try asking</Text>
|
|
203
|
+
<Text color={COLORS.textDim}> {SYMBOLS.arrow} Why did we choose this architecture?</Text>
|
|
204
|
+
<Text color={COLORS.textDim}> {SYMBOLS.arrow} What changed in auth recently?</Text>
|
|
205
|
+
<Text color={COLORS.textDim}> {SYMBOLS.arrow} Summarize known issues around embeddings</Text>
|
|
206
|
+
<Text color={COLORS.textDim}> {SYMBOLS.arrow} /search timeout bug</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<Box flexDirection="column" paddingX={1}>
|
|
214
|
+
<Text color={COLORS.brand} bold>Conversation{threadLabel}</Text>
|
|
215
|
+
<Text color={COLORS.border}>{separator(contentWidth - 4)}</Text>
|
|
216
|
+
<Box height={scrollHeight} flexDirection="column">
|
|
217
|
+
<ScrollView ref={scrollRef}>
|
|
218
|
+
{messages.map((message, index) => (
|
|
219
|
+
<MessageBlock
|
|
220
|
+
key={`${message.role}-${index}-${message.timestamp || index}`}
|
|
221
|
+
message={message}
|
|
222
|
+
width={contentWidth}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
{loading && (
|
|
226
|
+
<Box>
|
|
227
|
+
<Text color={COLORS.brand}><Spinner type="dots" />{' '}Thinking…</Text>
|
|
228
|
+
</Box>
|
|
229
|
+
)}
|
|
230
|
+
</ScrollView>
|
|
231
|
+
</Box>
|
|
232
|
+
</Box>
|
|
233
|
+
);
|
|
234
|
+
});
|