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.
@@ -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":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAQ,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAC,eAAe,EAAE;IACxD,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;IAClD,gBAAgB,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC,OAAO,EAAE;IACrD,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC;IACzB,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,eAAe,CAAC;IAC7F,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,eAAe,CAAC;CAC9F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,EAAE;IAC9C,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B,cAAc,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IAC7G,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE;IAC/E,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO;IACtC,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,EAAE,OAAO;IAC1C,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,EAAE,wCAAwC;IACzE,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC;IACnC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,eAAe,CAAC;CAC9F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,EAAE;IAChD,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,kCAAkC;IAC/D,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE;IACzE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC;IACvB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC7E,SAAS,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,eAAe,CAAC;CAC9F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE;IAC1C,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,uBAAuB;IACpD,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;IACjG,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,EAAE,gBAAgB;IACrD,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,iBAAiB,EAAE,OAAO,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACnF,aAAa,EAAE,OAAO,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3E,iBAAiB,EAAE,OAAO,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACpF,aAAa,EAAE,OAAO,CAAC,gBAAgB,CAAC;IACxC,WAAW,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,eAAe,CAAC;CAClG,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,EAAE;IAC9C,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE;IAC7B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;CAC/B,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
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