friday-code 1.0.0
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/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/friday.js +2 -0
- package/dist/core/engine/agent.d.ts +99 -0
- package/dist/core/engine/agent.js +317 -0
- package/dist/core/engine/agent.js.map +1 -0
- package/dist/core/providers/registry.d.ts +31 -0
- package/dist/core/providers/registry.js +213 -0
- package/dist/core/providers/registry.js.map +1 -0
- package/dist/core/tools/tools.d.ts +416 -0
- package/dist/core/tools/tools.js +338 -0
- package/dist/core/tools/tools.js.map +1 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +85 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +699 -0
- package/dist/db/schema.js +49 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/components/components.d.ts +81 -0
- package/dist/ui/components/components.js +416 -0
- package/dist/ui/components/components.js.map +1 -0
- package/dist/ui/theme/theme.d.ts +77 -0
- package/dist/ui/theme/theme.js +64 -0
- package/dist/ui/theme/theme.js.map +1 -0
- package/dist/ui/views/App.d.ts +6 -0
- package/dist/ui/views/App.js +629 -0
- package/dist/ui/views/App.js.map +1 -0
- package/dist/ui/views/InputBox.d.ts +16 -0
- package/dist/ui/views/InputBox.js +202 -0
- package/dist/ui/views/InputBox.js.map +1 -0
- package/dist/ui/views/ModelSelector.d.ts +7 -0
- package/dist/ui/views/ModelSelector.js +119 -0
- package/dist/ui/views/ModelSelector.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
export const conversations = sqliteTable('conversations', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
title: text('title').notNull().default('New Chat'),
|
|
6
|
+
workingDirectory: text('working_directory').notNull(),
|
|
7
|
+
modelId: text('model_id'),
|
|
8
|
+
providerId: text('provider_id'),
|
|
9
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
10
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
11
|
+
});
|
|
12
|
+
export const messages = sqliteTable('messages', {
|
|
13
|
+
id: text('id').primaryKey(),
|
|
14
|
+
conversationId: text('conversation_id').notNull().references(() => conversations.id, { onDelete: 'cascade' }),
|
|
15
|
+
role: text('role', { enum: ['user', 'assistant', 'system', 'tool'] }).notNull(),
|
|
16
|
+
content: text('content').notNull(),
|
|
17
|
+
reasoning: text('reasoning'),
|
|
18
|
+
toolCalls: text('tool_calls'), // JSON
|
|
19
|
+
toolResults: text('tool_results'), // JSON
|
|
20
|
+
tokenUsage: text('token_usage'), // JSON {promptTokens, completionTokens}
|
|
21
|
+
finishReason: text('finish_reason'),
|
|
22
|
+
modelId: text('model_id'),
|
|
23
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
24
|
+
});
|
|
25
|
+
export const providers = sqliteTable('providers', {
|
|
26
|
+
id: text('id').primaryKey(), // 'openai', 'anthropic', 'ollama'
|
|
27
|
+
name: text('name').notNull(),
|
|
28
|
+
type: text('type', { enum: ['openai', 'anthropic', 'ollama'] }).notNull(),
|
|
29
|
+
apiKey: text('api_key'),
|
|
30
|
+
baseUrl: text('base_url'),
|
|
31
|
+
isEnabled: integer('is_enabled', { mode: 'boolean' }).notNull().default(true),
|
|
32
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
33
|
+
});
|
|
34
|
+
export const models = sqliteTable('models', {
|
|
35
|
+
id: text('id').primaryKey(), // e.g. 'openai:gpt-4o'
|
|
36
|
+
providerId: text('provider_id').notNull().references(() => providers.id, { onDelete: 'cascade' }),
|
|
37
|
+
modelId: text('model_id').notNull(), // e.g. 'gpt-4o'
|
|
38
|
+
name: text('name').notNull(),
|
|
39
|
+
supportsStreaming: integer('supports_streaming', { mode: 'boolean' }).default(true),
|
|
40
|
+
supportsTools: integer('supports_tools', { mode: 'boolean' }).default(true),
|
|
41
|
+
supportsReasoning: integer('supports_reasoning', { mode: 'boolean' }).default(false),
|
|
42
|
+
contextWindow: integer('context_window'),
|
|
43
|
+
lastFetched: integer('last_fetched', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
44
|
+
});
|
|
45
|
+
export const settings = sqliteTable('settings', {
|
|
46
|
+
key: text('key').primaryKey(),
|
|
47
|
+
value: text('value').notNull(),
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import App from './ui/views/App.js';
|
|
5
|
+
import { initializeDatabase } from './db/index.js';
|
|
6
|
+
import 'dotenv/config';
|
|
7
|
+
// Initialize database tables
|
|
8
|
+
initializeDatabase();
|
|
9
|
+
// Parse CLI arguments
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
let initialDirectory = process.cwd();
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg === '--dir' || arg === '-d') {
|
|
15
|
+
initialDirectory = args[i + 1] || process.cwd();
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
else if (arg === '--help' || arg === '-h') {
|
|
19
|
+
console.log(`
|
|
20
|
+
◆ Friday Code — Terminal AI Agent
|
|
21
|
+
|
|
22
|
+
Usage: friday [options]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
-d, --dir <path> Set working directory (default: current directory)
|
|
26
|
+
-h, --help Show this help message
|
|
27
|
+
-v, --version Show version
|
|
28
|
+
|
|
29
|
+
Inside Friday Code:
|
|
30
|
+
/help Show available commands
|
|
31
|
+
/model Select AI model
|
|
32
|
+
/provider Manage providers & API keys
|
|
33
|
+
/scope <path> Change working directory
|
|
34
|
+
/config View/set configuration
|
|
35
|
+
/clear Clear conversation
|
|
36
|
+
/new Start a fresh conversation
|
|
37
|
+
/history Show history count
|
|
38
|
+
/exit Exit Friday Code
|
|
39
|
+
@file Mention a file for context
|
|
40
|
+
`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
else if (arg === '--version' || arg === '-v') {
|
|
44
|
+
console.log('friday-code v1.0.0');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Render the app
|
|
49
|
+
const { waitUntilExit } = render(React.createElement(App, { initialDirectory }), {
|
|
50
|
+
exitOnCtrlC: false, // We handle Ctrl+C ourselves
|
|
51
|
+
});
|
|
52
|
+
waitUntilExit().then(() => {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,GAAG,MAAM,mBAAmB,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,eAAe,CAAC;AAEvB,6BAA6B;AAC7B,kBAAkB,EAAE,CAAC;AAErB,sBAAsB;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,IAAI,gBAAgB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;AAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACpC,gBAAgB,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChD,CAAC,EAAE,CAAC;IACN,CAAC;SAAM,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;CAqBf,CAAC,CAAC;QACC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;SAAM,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,iBAAiB;AACjB,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAC9B,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,EAAE,gBAAgB,EAAE,CAAC,EAC9C;IACE,WAAW,EAAE,KAAK,EAAE,6BAA6B;CAClD,CACF,CAAC;AAEF,aAAa,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;IACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type FC, type ReactNode } from 'react';
|
|
2
|
+
export interface TimelineNodeView {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: 'phase' | 'step' | 'thinking' | 'tool-call' | 'tool-result' | 'text' | 'subagent' | 'done' | 'approval';
|
|
5
|
+
label: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
stepNumber?: number;
|
|
8
|
+
status?: 'running' | 'done' | 'error';
|
|
9
|
+
toolCallId?: string;
|
|
10
|
+
}
|
|
11
|
+
export type RunStatusView = 'running' | 'complete' | 'error' | 'cancelled';
|
|
12
|
+
export declare const Spinner: FC<{
|
|
13
|
+
label?: string;
|
|
14
|
+
color?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const Panel: FC<{
|
|
17
|
+
title?: string;
|
|
18
|
+
borderColor?: string;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
width?: number | string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const HeaderBar: FC<{
|
|
23
|
+
provider?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
scope?: string;
|
|
26
|
+
status: string;
|
|
27
|
+
compact?: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const StatusBar: FC<{
|
|
30
|
+
tokens?: {
|
|
31
|
+
prompt: number;
|
|
32
|
+
completion: number;
|
|
33
|
+
};
|
|
34
|
+
status?: string;
|
|
35
|
+
engineStatus: string;
|
|
36
|
+
compact?: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const KeyboardBar: FC<{
|
|
39
|
+
isGenerating: boolean;
|
|
40
|
+
compact?: boolean;
|
|
41
|
+
pendingApproval?: boolean;
|
|
42
|
+
canRetry?: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
export declare const WelcomeScreen: FC<{
|
|
45
|
+
model?: string;
|
|
46
|
+
provider?: string;
|
|
47
|
+
scope?: string;
|
|
48
|
+
compact?: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
export declare const MessageBubble: FC<{
|
|
51
|
+
role: 'user' | 'assistant';
|
|
52
|
+
content: string;
|
|
53
|
+
isStreaming?: boolean;
|
|
54
|
+
viewportWidth?: number;
|
|
55
|
+
}>;
|
|
56
|
+
export declare const RunTimelineCard: FC<{
|
|
57
|
+
status: RunStatusView;
|
|
58
|
+
nodes: TimelineNodeView[];
|
|
59
|
+
isStreaming?: boolean;
|
|
60
|
+
compact?: boolean;
|
|
61
|
+
viewportWidth?: number;
|
|
62
|
+
viewportHeight?: number;
|
|
63
|
+
}>;
|
|
64
|
+
export declare const CollapsedRunCard: FC<{
|
|
65
|
+
nodes: TimelineNodeView[];
|
|
66
|
+
status: RunStatusView;
|
|
67
|
+
}>;
|
|
68
|
+
export declare const Toast: FC<{
|
|
69
|
+
message: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
}>;
|
|
72
|
+
export declare const HelpView: FC;
|
|
73
|
+
export declare const Divider: FC<{
|
|
74
|
+
label?: string;
|
|
75
|
+
}>;
|
|
76
|
+
type ToneType = 'info' | 'success' | 'warning' | 'error' | 'muted' | 'accent';
|
|
77
|
+
export declare function toneColor(tone: ToneType): string;
|
|
78
|
+
export declare function toneBullet(tone: ToneType): string;
|
|
79
|
+
export declare function summarizeBlock(text: string | undefined, maxLines: number): string;
|
|
80
|
+
export declare function truncateInline(text: string, limit: number): string;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { colors, icons } from '../theme/theme.js';
|
|
5
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
6
|
+
// ═══════════════════════════════════════════════════
|
|
7
|
+
// PRIMITIVES
|
|
8
|
+
// ═══════════════════════════════════════════════════
|
|
9
|
+
export const Spinner = ({ label, color = colors.primary, }) => {
|
|
10
|
+
const [index, setIndex] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const t = setInterval(() => setIndex(i => (i + 1) % SPINNER_FRAMES.length), 80);
|
|
13
|
+
return () => clearInterval(t);
|
|
14
|
+
}, []);
|
|
15
|
+
return (_jsxs(Text, { color: color, children: [SPINNER_FRAMES[index], label ? ` ${label}` : ''] }));
|
|
16
|
+
};
|
|
17
|
+
export const Panel = ({ title, borderColor = colors.faint, children, width }) => (_jsxs(Box, { flexDirection: "column", width: width, children: [title ? _jsx(Text, { color: borderColor, children: title }) : null, _jsx(Box, { flexDirection: "column", paddingLeft: 1, children: children })] }));
|
|
18
|
+
// ═══════════════════════════════════════════════════
|
|
19
|
+
// SPINE HELPERS
|
|
20
|
+
// ═══════════════════════════════════════════════════
|
|
21
|
+
const SL = ({ children: text, color = colors.secondary }) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: color, children: text })] }));
|
|
22
|
+
const SG = () => _jsx(Text, { color: colors.faint, children: ' \u2502' });
|
|
23
|
+
// ═══════════════════════════════════════════════════
|
|
24
|
+
// HEADER
|
|
25
|
+
// ═══════════════════════════════════════════════════
|
|
26
|
+
export const HeaderBar = ({ provider = '', model = 'none', scope = '.', status }) => {
|
|
27
|
+
const ml = provider ? `${provider}/${model}` : model;
|
|
28
|
+
const isRunning = status === 'running';
|
|
29
|
+
const [tick, setTick] = useState(0);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!isRunning)
|
|
32
|
+
return;
|
|
33
|
+
const t = setInterval(() => setTick(i => (i + 1) % SPINNER_FRAMES.length), 80);
|
|
34
|
+
return () => clearInterval(t);
|
|
35
|
+
}, [isRunning]);
|
|
36
|
+
const statusText = status === 'idle' ? ''
|
|
37
|
+
: isRunning ? ` ${SPINNER_FRAMES[tick]} working`
|
|
38
|
+
: ` · ${status}`;
|
|
39
|
+
const statusColor = status === 'complete' ? colors.success
|
|
40
|
+
: status === 'error' ? colors.error
|
|
41
|
+
: isRunning ? colors.warn
|
|
42
|
+
: colors.primary;
|
|
43
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.primary, bold: true, children: icons.friday }), _jsx(Text, { color: colors.muted, children: ' friday' }), _jsx(Text, { color: colors.subtle, children: ' · ' }), _jsx(Text, { color: colors.secondary, children: ml }), _jsx(Text, { color: colors.subtle, children: ' · ' }), _jsx(Text, { color: colors.muted, children: scope }), statusText ? _jsx(Text, { color: statusColor, children: statusText }) : null] }) }));
|
|
44
|
+
};
|
|
45
|
+
// ═══════════════════════════════════════════════════
|
|
46
|
+
// STATUS BAR
|
|
47
|
+
// ═══════════════════════════════════════════════════
|
|
48
|
+
export const StatusBar = ({ tokens, status, engineStatus }) => {
|
|
49
|
+
if (engineStatus === 'idle' && !tokens) {
|
|
50
|
+
return _jsx(Text, { color: colors.faint, children: ' ready' });
|
|
51
|
+
}
|
|
52
|
+
const parts = [
|
|
53
|
+
status || engineStatus,
|
|
54
|
+
tokens ? `${tokens.prompt}+${tokens.completion} tok` : '',
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
return _jsx(Text, { color: colors.faint, children: ` ${parts.join(' · ')}` });
|
|
57
|
+
};
|
|
58
|
+
// ═══════════════════════════════════════════════════
|
|
59
|
+
// KEYBOARD BAR
|
|
60
|
+
// ═══════════════════════════════════════════════════
|
|
61
|
+
export const KeyboardBar = ({ isGenerating, compact = false, pendingApproval = false, canRetry = false }) => {
|
|
62
|
+
if (pendingApproval) {
|
|
63
|
+
return _jsx(Text, { color: colors.warn, children: ` Enter approve · Esc deny` });
|
|
64
|
+
}
|
|
65
|
+
if (canRetry && !isGenerating) {
|
|
66
|
+
return _jsx(Text, { color: colors.faint, children: ` r retry · Enter send · Ctrl+D exit` });
|
|
67
|
+
}
|
|
68
|
+
const keys = compact
|
|
69
|
+
? ['Enter send', 'Tab complete', isGenerating ? 'Ctrl+C stop' : 'Ctrl+D exit']
|
|
70
|
+
: ['Enter send', 'Tab complete', '↑ history', isGenerating ? 'Ctrl+C stop' : 'Ctrl+D exit'];
|
|
71
|
+
return _jsx(Text, { color: colors.faint, children: ` ${keys.join(' · ')}` });
|
|
72
|
+
};
|
|
73
|
+
// ═══════════════════════════════════════════════════
|
|
74
|
+
// WELCOME SCREEN
|
|
75
|
+
// ═══════════════════════════════════════════════════
|
|
76
|
+
export const WelcomeScreen = ({ model = 'not set', provider = '', scope = '.', compact = false }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.primary, bold: true, children: ' \u25c8 friday code' }), _jsx(SG, {}), _jsx(SL, { color: colors.secondary, children: `model \u2192 ${provider ? `${provider}/` : ''}${model}` }), _jsx(SL, { color: colors.secondary, children: `scope \u2192 ${scope}` }), _jsx(SG, {}), _jsx(SL, { color: colors.text, children: 'Set a goal to begin.' }), _jsx(SG, {}), _jsx(SL, { color: colors.muted, children: '\u00b7 explain this project' }), _jsx(SL, { color: colors.muted, children: '\u00b7 find and fix bugs in @file' }), !compact && _jsx(SL, { color: colors.muted, children: '\u00b7 refactor this module for clarity' }), _jsx(SL, { color: colors.muted, children: '\u00b7 /help for all commands' })] }));
|
|
77
|
+
// ═══════════════════════════════════════════════════
|
|
78
|
+
// MESSAGE BUBBLE
|
|
79
|
+
// ═══════════════════════════════════════════════════
|
|
80
|
+
export const MessageBubble = ({ role, content, isStreaming, viewportWidth }) => {
|
|
81
|
+
if (role === 'user') {
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.info, children: ' \u25cf ' }), _jsx(Text, { color: colors.text, bold: true, children: 'you' })] }), content.split('\n').slice(0, 6).map((line, i) => (_jsx(SL, { color: colors.text, children: line }, i)))] }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.primary, children: ' \u25c8 ' }), _jsx(Text, { color: colors.text, children: 'friday' })] }), renderSpineContent(content, false, viewportWidth), isStreaming && _jsx(Text, { color: colors.primary, children: ' \u2502 \u258c' })] }));
|
|
85
|
+
};
|
|
86
|
+
// ═══════════════════════════════════════════════════
|
|
87
|
+
// RUN TIMELINE CARD
|
|
88
|
+
// ═══════════════════════════════════════════════════
|
|
89
|
+
export const RunTimelineCard = ({ status, nodes, isStreaming = false, compact = false, viewportWidth, viewportHeight }) => {
|
|
90
|
+
const windowSize = compact ? 10 : 18;
|
|
91
|
+
const visible = nodes.slice(-windowSize);
|
|
92
|
+
const hidden = Math.max(0, nodes.length - visible.length);
|
|
93
|
+
// Calculate how many lines text content can use.
|
|
94
|
+
// Reserve space for: header(2), input(3), keyboard(1), status(1), hidden-indicator(1)
|
|
95
|
+
const fixedOverhead = 8;
|
|
96
|
+
// Each non-text node uses ~1.5 lines (content + gap)
|
|
97
|
+
const nonTextNodes = visible.filter(n => n.kind !== 'text');
|
|
98
|
+
const nonTextLines = Math.ceil(nonTextNodes.length * 1.6) + 3; // +3 for done SG, user msg
|
|
99
|
+
const maxTextLines = Math.max(4, (viewportHeight || 24) - fixedOverhead - nonTextLines);
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hidden > 0 && (_jsx(Text, { color: colors.faint, children: ` ┄ ${hidden} earlier event${hidden === 1 ? '' : 's'}` })), visible.map((node, i) => {
|
|
101
|
+
const isLast = i === visible.length - 1;
|
|
102
|
+
const streaming = isStreaming && isLast && node.status === 'running';
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", children: [renderSpineNode(node, streaming, compact, viewportWidth, node.kind === 'text' ? maxTextLines : undefined), !isLast && node.kind !== 'tool-call' && _jsx(SG, {})] }, node.id));
|
|
104
|
+
}), status === 'error' && _jsx(Text, { color: colors.error, children: ' ✕ run failed' }), status === 'cancelled' && _jsx(Text, { color: colors.warn, children: ' ! cancelled' })] }));
|
|
105
|
+
};
|
|
106
|
+
// ═══════════════════════════════════════════════════
|
|
107
|
+
// COLLAPSED RUN SUMMARY (for past completed runs)
|
|
108
|
+
// ═══════════════════════════════════════════════════
|
|
109
|
+
export const CollapsedRunCard = ({ nodes, status }) => {
|
|
110
|
+
const toolCalls = nodes.filter(n => n.kind === 'tool-call');
|
|
111
|
+
const textNode = [...nodes].reverse().find(n => n.kind === 'text');
|
|
112
|
+
const preview = textNode?.detail?.replace(/[\n\r]+/g, ' ').replace(/\*\*/g, '').replace(/`/g, '').replace(/\s{2,}/g, ' ').trim().slice(0, 80) || '';
|
|
113
|
+
const doneNode = nodes.find(n => n.kind === 'done');
|
|
114
|
+
const stepCount = doneNode?.detail?.match(/(\d+)\s*step/)?.[1] || doneNode?.label?.match(/(\d+)\s*step/)?.[1] || '1';
|
|
115
|
+
const statusIcon = status === 'complete' ? icons.ok : status === 'error' ? icons.fail : '○';
|
|
116
|
+
const statusColor = status === 'complete' ? colors.success : status === 'error' ? colors.error : colors.faint;
|
|
117
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { color: statusColor, children: ` ${statusIcon} ` }), _jsx(Text, { color: colors.muted, children: `${stepCount} step${stepCount === '1' ? '' : 's'}` }), toolCalls.length > 0 && _jsx(Text, { color: colors.faint, children: ` · ${toolCalls.length} tool${toolCalls.length === 1 ? '' : 's'}` }), preview ? _jsx(Text, { color: colors.faint, children: ` · ${preview}${preview.length >= 80 ? '…' : ''}` }) : null] }) }));
|
|
118
|
+
};
|
|
119
|
+
// ═══════════════════════════════════════════════════
|
|
120
|
+
// TOAST, HELP, DIVIDER
|
|
121
|
+
// ═══════════════════════════════════════════════════
|
|
122
|
+
export const Toast = ({ message, type = 'info' }) => (_jsx(Text, { color: toneColor(normalizeTone(type)), children: ` ${toneBullet(normalizeTone(type))} ${message}` }));
|
|
123
|
+
const helpItems = [
|
|
124
|
+
['/help', 'commands & shortcuts'],
|
|
125
|
+
['/model', 'switch model'],
|
|
126
|
+
['/provider', 'manage provider keys'],
|
|
127
|
+
['/scope <path>', 'change working scope'],
|
|
128
|
+
['/clear', 'clear conversation'],
|
|
129
|
+
['/new', 'new conversation'],
|
|
130
|
+
['/history', 'message count'],
|
|
131
|
+
['/exit', 'quit friday code'],
|
|
132
|
+
['@file', 'inject file as context'],
|
|
133
|
+
];
|
|
134
|
+
export const HelpView = () => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.primary, children: ' \u25c8 commands' }), helpItems.map(([cmd, desc]) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.text, children: cmd.padEnd(16) }), _jsx(Text, { color: colors.muted, children: desc })] }, cmd))), _jsx(Text, { color: colors.faint, children: ' \u2502' }), _jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.subtle, children: `Enter send \u00b7 Tab complete \u00b7 \u2191 history \u00b7 Ctrl+C stop` })] })] }));
|
|
135
|
+
export const Divider = ({ label }) => (_jsx(Text, { color: colors.faint, children: ` ${'─'.repeat(4)}${label ? ` ${label} ` : ''}${'─'.repeat(16)}` }));
|
|
136
|
+
// ═══════════════════════════════════════════════════
|
|
137
|
+
// SPINE NODE RENDERER
|
|
138
|
+
// ═══════════════════════════════════════════════════
|
|
139
|
+
function renderSpineNode(node, streaming, compact, viewportWidth, maxTextLines) {
|
|
140
|
+
const ic = nodeIcon(node);
|
|
141
|
+
const col = nodeColor(node);
|
|
142
|
+
const detailLines = compact ? 1 : 2;
|
|
143
|
+
// Available text width for detail content (5 = " │ " prefix)
|
|
144
|
+
const maxDetail = viewportWidth ? Math.max(20, viewportWidth - 8) : (compact ? 48 : 72);
|
|
145
|
+
const detailLimit = Math.min(maxDetail, compact ? 60 : 120);
|
|
146
|
+
switch (node.kind) {
|
|
147
|
+
case 'thinking': {
|
|
148
|
+
const thinkPreview = node.detail ? trunc(summarizeBlock(node.detail, detailLines), detailLimit) : '';
|
|
149
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: col, children: ` ${ic} ` }), _jsx(Text, { color: colors.muted, children: 'thinking' }), node.stepNumber ? _jsx(Text, { color: colors.subtle, children: ` \u00b7 step ${node.stepNumber}` }) : null, streaming ? _jsx(Text, { color: colors.primary, children: ' \u258c' }) : null] }), _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.subtle, children: thinkPreview ? `\u2504 ${thinkPreview}` : '' })] })] }));
|
|
150
|
+
}
|
|
151
|
+
case 'tool-call': {
|
|
152
|
+
const toolName = extractToolName(node.label);
|
|
153
|
+
// Show args inline after tool name to avoid re-render artifacts
|
|
154
|
+
const argsDisplay = node.detail ? ` → ${trunc(node.detail, Math.max(10, maxDetail - toolName.length - 4))}` : '';
|
|
155
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: col, children: ` ${ic} ` }), _jsx(Text, { color: node.status === 'running' ? colors.warn : colors.secondary, children: toolName }), argsDisplay ? _jsx(Text, { color: colors.subtle, children: argsDisplay }) : null, streaming ? _jsx(Text, { color: colors.warn, children: ' \u258c' }) : null] }));
|
|
156
|
+
}
|
|
157
|
+
case 'tool-result': {
|
|
158
|
+
// Strip newlines to prevent multi-line bleeding without spine prefix
|
|
159
|
+
const resultDetail = (node.detail || node.label).replace(/[\n\r]+/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
160
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: node.status === 'error' ? colors.error : colors.success, children: `${node.status === 'error' ? '\u2715' : '\u2713'} ` }), _jsx(Text, { color: node.status === 'error' ? colors.error : colors.muted, children: trunc(resultDetail, maxDetail) })] }));
|
|
161
|
+
}
|
|
162
|
+
case 'phase':
|
|
163
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: colors.primary, children: ` ${icons.running} ` }), _jsx(Text, { color: colors.secondary, children: node.label })] }));
|
|
164
|
+
case 'step':
|
|
165
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: node.status === 'done' ? colors.success : node.status === 'error' ? colors.error : colors.primary, children: ` ${node.status === 'done' ? '\u2713' : node.status === 'error' ? '\u2715' : '\u25c6'} ` }), _jsx(Text, { color: colors.secondary, children: node.label }), node.detail ? _jsx(Text, { color: colors.subtle, children: ` \u00b7 ${trunc(node.detail, Math.min(maxDetail, 40))}` }) : null] }));
|
|
166
|
+
case 'text': {
|
|
167
|
+
// Trim leading/trailing blank lines to avoid double spine gaps
|
|
168
|
+
const trimmedDetail = (node.detail || '').replace(/^\n+/, '').replace(/(\n\s*)+$/, '');
|
|
169
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.primary, children: ' \u25c8 ' }), _jsx(Text, { color: colors.text, children: streaming ? 'responding' : 'response' })] }, "resp-hdr"), trimmedDetail ? renderSpineContent(trimmedDetail, compact, viewportWidth, maxTextLines) : null, streaming && _jsx(Text, { color: colors.primary, children: ' \u2502 \u258c' }, "resp-cursor")] }));
|
|
170
|
+
}
|
|
171
|
+
case 'subagent':
|
|
172
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: colors.info, children: ` ${icons.detach} ` }), _jsx(Text, { color: colors.secondary, children: node.label })] }));
|
|
173
|
+
case 'approval':
|
|
174
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.warn, children: ` ${icons.warn} ` }), _jsx(Text, { color: colors.text, bold: true, children: 'approve ' }), _jsx(Text, { color: colors.secondary, children: node.label }), node.detail ? _jsx(Text, { color: colors.faint, children: ` ${icons.arrow} ${node.detail}` }) : null] }), node.status === 'running' && (_jsxs(Text, { color: colors.muted, children: [' \u2502 ', _jsx(Text, { color: colors.warn, children: 'Enter' }), ' approve \u00b7 ', _jsx(Text, { color: colors.error, children: 'Esc' }), ' deny'] })), node.status === 'done' && (_jsx(Text, { color: colors.success, children: ' \u2502 \u2713 approved' })), node.status === 'error' && (_jsx(Text, { color: colors.error, children: ' \u2502 \u2715 denied' }))] }));
|
|
175
|
+
case 'done':
|
|
176
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: colors.success, children: ' \u2713 ' }), _jsx(Text, { color: colors.secondary, children: 'done' }), node.detail ? _jsx(Text, { color: colors.muted, children: ` \u00b7 ${node.detail}` }) : null] }));
|
|
177
|
+
default:
|
|
178
|
+
return _jsx(Text, { color: colors.muted, children: ` \u00b7 ${node.label}` });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function extractToolName(label) {
|
|
182
|
+
const parts = label.split('\u00b7');
|
|
183
|
+
return (parts.length > 2 ? parts[parts.length - 1] : parts[0] || label).trim();
|
|
184
|
+
}
|
|
185
|
+
// ═══════════════════════════════════════════════════
|
|
186
|
+
// MARKDOWN SPINE CONTENT RENDERER
|
|
187
|
+
// ═══════════════════════════════════════════════════
|
|
188
|
+
function wrapText(text, maxWidth) {
|
|
189
|
+
if (maxWidth <= 0)
|
|
190
|
+
maxWidth = 80;
|
|
191
|
+
const words = text.split(' ');
|
|
192
|
+
let currentLine = '';
|
|
193
|
+
const lines = [];
|
|
194
|
+
for (const word of words) {
|
|
195
|
+
if (currentLine && (currentLine.length + 1 + word.length) > maxWidth) {
|
|
196
|
+
lines.push(currentLine);
|
|
197
|
+
currentLine = word;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (currentLine)
|
|
204
|
+
lines.push(currentLine);
|
|
205
|
+
return lines.length > 0 ? lines : [''];
|
|
206
|
+
}
|
|
207
|
+
function renderSpineContent(content, compact, maxWidth, lineLimitOverride) {
|
|
208
|
+
const lines = content.split('\n');
|
|
209
|
+
const result = [];
|
|
210
|
+
let inCode = false;
|
|
211
|
+
let codeLang = '';
|
|
212
|
+
let codeLines = [];
|
|
213
|
+
const lineLimit = lineLimitOverride ?? (compact ? 6 : 16);
|
|
214
|
+
const textWidth = maxWidth ? maxWidth - 5 : 100; // 5 = ' │ ' prefix
|
|
215
|
+
for (let i = 0; i < Math.min(lines.length, lineLimit); i++) {
|
|
216
|
+
const line = lines[i];
|
|
217
|
+
if (line.startsWith('```')) {
|
|
218
|
+
if (inCode) {
|
|
219
|
+
if (codeLang) {
|
|
220
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.subtle, children: `\u2500 ${codeLang} \u2500` })] }, `ch-${i}`));
|
|
221
|
+
}
|
|
222
|
+
for (const [ci, cl] of codeLines.entries()) {
|
|
223
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.primaryBright, children: cl })] }, `c-${i}-${ci}`));
|
|
224
|
+
}
|
|
225
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.subtle, children: '\u2500' })] }, `ce-${i}`));
|
|
226
|
+
codeLines = [];
|
|
227
|
+
codeLang = '';
|
|
228
|
+
inCode = false;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
inCode = true;
|
|
232
|
+
codeLang = line.slice(3).trim();
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (inCode) {
|
|
237
|
+
codeLines.push(line);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
// Trim leading whitespace for pattern matching, but preserve indent level
|
|
241
|
+
const trimmed = line.trimStart();
|
|
242
|
+
const indent = line.length - trimmed.length;
|
|
243
|
+
const indentStr = indent > 0 ? ' '.repeat(Math.min(indent, 4)) : '';
|
|
244
|
+
if (trimmed.startsWith('# ')) {
|
|
245
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.primary, bold: true, children: trimmed.slice(2) })] }, i));
|
|
246
|
+
}
|
|
247
|
+
else if (trimmed.startsWith('## ')) {
|
|
248
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.primaryBright, bold: true, children: trimmed.slice(3) })] }, i));
|
|
249
|
+
}
|
|
250
|
+
else if (trimmed.startsWith('### ')) {
|
|
251
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.secondary, bold: true, children: trimmed.slice(4) })] }, i));
|
|
252
|
+
}
|
|
253
|
+
else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
254
|
+
const prefix = `${indentStr}\u00b7 `;
|
|
255
|
+
const contentText = trimmed.slice(2);
|
|
256
|
+
const prefixWidth = 4 + prefix.length; // ' │ ' + prefix
|
|
257
|
+
const wrapWidth = textWidth - prefix.length;
|
|
258
|
+
const wrapped = wrapText(contentText, wrapWidth);
|
|
259
|
+
for (const [wi, wl] of wrapped.entries()) {
|
|
260
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), wi === 0 ? _jsx(Text, { color: colors.subtle, children: prefix }) : _jsx(Text, { children: ' '.repeat(prefix.length) }), renderInlineMarkdown(wl, `md-${i}-${wi}`)] }, `${i}-${wi}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (trimmed.match(/^\d+\. /)) {
|
|
264
|
+
const numEnd = trimmed.indexOf('. ');
|
|
265
|
+
const num = trimmed.slice(0, numEnd + 2);
|
|
266
|
+
const rest = trimmed.slice(numEnd + 2);
|
|
267
|
+
const prefix = `${indentStr}${num}`;
|
|
268
|
+
const wrapWidth = textWidth - prefix.length;
|
|
269
|
+
const wrapped = wrapText(rest, wrapWidth);
|
|
270
|
+
for (const [wi, wl] of wrapped.entries()) {
|
|
271
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), wi === 0 ? _jsx(Text, { color: colors.subtle, children: prefix }) : _jsx(Text, { children: ' '.repeat(prefix.length) }), renderInlineMarkdown(wl, `md-${i}-${wi}`)] }, `${i}-${wi}`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (trimmed === '') {
|
|
275
|
+
result.push(_jsx(Text, { color: colors.faint, children: ' \u2502' }, i));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Manually wrap long lines to keep spine prefix on each visual line
|
|
279
|
+
const wrapped = wrapText(line, textWidth);
|
|
280
|
+
for (const [wi, wl] of wrapped.entries()) {
|
|
281
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), renderInlineMarkdown(wl, `md-${i}-${wi}`)] }, `${i}-${wi}`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (lines.length > lineLimit) {
|
|
286
|
+
result.push(_jsx(Text, { color: colors.faint, children: ` \u2502 \u2504 ${lines.length - lineLimit} more lines` }, "trunc"));
|
|
287
|
+
}
|
|
288
|
+
if (inCode && codeLines.length > 0) {
|
|
289
|
+
for (const [ci, cl] of codeLines.entries()) {
|
|
290
|
+
result.push(_jsxs(Text, { children: [_jsx(Text, { color: colors.faint, children: ' \u2502 ' }), _jsx(Text, { color: colors.primaryBright, children: cl })] }, `ct-${ci}`));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
// ═══════════════════════════════════════════════════
|
|
296
|
+
// INLINE MARKDOWN RENDERER
|
|
297
|
+
// ═══════════════════════════════════════════════════
|
|
298
|
+
function renderInlineMarkdown(text, keyPrefix) {
|
|
299
|
+
// Parse inline markdown: **bold**, *italic*, `code`
|
|
300
|
+
const parts = [];
|
|
301
|
+
let remaining = text;
|
|
302
|
+
let idx = 0;
|
|
303
|
+
while (remaining.length > 0) {
|
|
304
|
+
// Try `code` first (highest priority - no nesting inside code)
|
|
305
|
+
const codeMatch = remaining.match(/^`([^`]+)`/);
|
|
306
|
+
if (codeMatch) {
|
|
307
|
+
parts.push(_jsx(Text, { color: colors.accent, children: codeMatch[1] }, `${keyPrefix}-${idx}`));
|
|
308
|
+
remaining = remaining.slice(codeMatch[0].length);
|
|
309
|
+
idx++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Try **bold**
|
|
313
|
+
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
|
|
314
|
+
if (boldMatch) {
|
|
315
|
+
parts.push(_jsx(Text, { bold: true, children: boldMatch[1] }, `${keyPrefix}-${idx}`));
|
|
316
|
+
remaining = remaining.slice(boldMatch[0].length);
|
|
317
|
+
idx++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// Try *italic*
|
|
321
|
+
const italicMatch = remaining.match(/^\*([^*]+)\*/);
|
|
322
|
+
if (italicMatch) {
|
|
323
|
+
parts.push(_jsx(Text, { italic: true, dimColor: true, children: italicMatch[1] }, `${keyPrefix}-${idx}`));
|
|
324
|
+
remaining = remaining.slice(italicMatch[0].length);
|
|
325
|
+
idx++;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// Find the next special character
|
|
329
|
+
const nextSpecial = remaining.search(/[`*]/);
|
|
330
|
+
if (nextSpecial === -1) {
|
|
331
|
+
// No more markdown — push rest as plain text
|
|
332
|
+
parts.push(_jsx(Text, { children: remaining }, `${keyPrefix}-${idx}`));
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
else if (nextSpecial === 0) {
|
|
336
|
+
// Special char that didn't match a pattern — treat as literal
|
|
337
|
+
parts.push(_jsx(Text, { children: remaining[0] }, `${keyPrefix}-${idx}`));
|
|
338
|
+
remaining = remaining.slice(1);
|
|
339
|
+
idx++;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Plain text before the next special
|
|
343
|
+
parts.push(_jsx(Text, { children: remaining.slice(0, nextSpecial) }, `${keyPrefix}-${idx}`));
|
|
344
|
+
remaining = remaining.slice(nextSpecial);
|
|
345
|
+
idx++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return _jsx(_Fragment, { children: parts });
|
|
349
|
+
}
|
|
350
|
+
// ═══════════════════════════════════════════════════
|
|
351
|
+
// HELPER FUNCTIONS
|
|
352
|
+
// ═══════════════════════════════════════════════════
|
|
353
|
+
function nodeIcon(node) {
|
|
354
|
+
switch (node.kind) {
|
|
355
|
+
case 'thinking': return '\u25c6';
|
|
356
|
+
case 'tool-call': return '\u25c7';
|
|
357
|
+
case 'tool-result': return node.status === 'error' ? '\u2715' : '\u2713';
|
|
358
|
+
case 'text': return '\u25c8';
|
|
359
|
+
case 'phase': return '\u25ce';
|
|
360
|
+
case 'step': return node.status === 'done' ? '\u2713' : node.status === 'error' ? '\u2715' : '\u25c6';
|
|
361
|
+
case 'subagent': return '\u25eb';
|
|
362
|
+
default: return '\u00b7';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function nodeColor(node) {
|
|
366
|
+
switch (node.kind) {
|
|
367
|
+
case 'thinking': return colors.muted;
|
|
368
|
+
case 'tool-call': return node.status === 'error' ? colors.error : colors.warn;
|
|
369
|
+
case 'tool-result': return node.status === 'error' ? colors.error : colors.success;
|
|
370
|
+
case 'text': return colors.primary;
|
|
371
|
+
case 'phase': return colors.primary;
|
|
372
|
+
case 'step': return node.status === 'error' ? colors.error : node.status === 'done' ? colors.success : colors.primary;
|
|
373
|
+
case 'subagent': return colors.info;
|
|
374
|
+
default: return colors.muted;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
export function toneColor(tone) {
|
|
378
|
+
switch (tone) {
|
|
379
|
+
case 'success': return colors.success;
|
|
380
|
+
case 'warning': return colors.warn;
|
|
381
|
+
case 'error': return colors.error;
|
|
382
|
+
case 'accent': return colors.primary;
|
|
383
|
+
case 'info': return colors.info;
|
|
384
|
+
default: return colors.muted;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
export function toneBullet(tone) {
|
|
388
|
+
switch (tone) {
|
|
389
|
+
case 'success': return '\u2713';
|
|
390
|
+
case 'warning': return '!';
|
|
391
|
+
case 'error': return '\u2715';
|
|
392
|
+
case 'accent': return '\u25c6';
|
|
393
|
+
case 'info': return 'i';
|
|
394
|
+
default: return '\u00b7';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function normalizeTone(type) {
|
|
398
|
+
if (type === 'success' || type === 'warning' || type === 'error' || type === 'accent' || type === 'info')
|
|
399
|
+
return type;
|
|
400
|
+
return 'muted';
|
|
401
|
+
}
|
|
402
|
+
export function summarizeBlock(text, maxLines) {
|
|
403
|
+
if (!text)
|
|
404
|
+
return '';
|
|
405
|
+
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
|
406
|
+
if (lines.length <= maxLines)
|
|
407
|
+
return lines.join(' ');
|
|
408
|
+
return `${lines.slice(0, maxLines).join(' ')} \u2026`;
|
|
409
|
+
}
|
|
410
|
+
function trunc(text, limit) {
|
|
411
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}\u2026` : text;
|
|
412
|
+
}
|
|
413
|
+
export function truncateInline(text, limit) {
|
|
414
|
+
return trunc(text, limit);
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=components.js.map
|