@calliopelabs/cli 0.8.20 → 2.0.2
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/dist/agents/agent-config-loader.d.ts +60 -0
- package/dist/agents/agent-config-loader.d.ts.map +1 -0
- package/dist/agents/agent-config-loader.js +402 -0
- package/dist/agents/agent-config-loader.js.map +1 -0
- package/dist/agents/agent-config-presets.d.ts +10 -0
- package/dist/agents/agent-config-presets.d.ts.map +1 -0
- package/dist/agents/agent-config-presets.js +940 -0
- package/dist/agents/agent-config-presets.js.map +1 -0
- package/dist/agents/agent-config-types.d.ts +145 -0
- package/dist/agents/agent-config-types.d.ts.map +1 -0
- package/dist/agents/agent-config-types.js +12 -0
- package/dist/agents/agent-config-types.js.map +1 -0
- package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
- package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
- package/dist/{agterm → agents}/agent-detection.js +21 -5
- package/dist/agents/agent-detection.js.map +1 -0
- package/dist/agents/aggregator.d.ts +19 -0
- package/dist/agents/aggregator.d.ts.map +1 -0
- package/dist/agents/aggregator.js +141 -0
- package/dist/agents/aggregator.js.map +1 -0
- package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
- package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
- package/dist/{agterm → agents}/cli-backend.js +90 -12
- package/dist/agents/cli-backend.js.map +1 -0
- package/dist/agents/council-types.d.ts +113 -0
- package/dist/agents/council-types.d.ts.map +1 -0
- package/dist/agents/council-types.js +81 -0
- package/dist/agents/council-types.js.map +1 -0
- package/dist/agents/council.d.ts +107 -0
- package/dist/agents/council.d.ts.map +1 -0
- package/dist/agents/council.js +586 -0
- package/dist/agents/council.js.map +1 -0
- package/dist/agents/decomposer.d.ts +33 -0
- package/dist/agents/decomposer.d.ts.map +1 -0
- package/dist/agents/decomposer.js +138 -0
- package/dist/agents/decomposer.js.map +1 -0
- package/dist/agents/dynamic-tools.d.ts +52 -0
- package/dist/agents/dynamic-tools.d.ts.map +1 -0
- package/dist/agents/dynamic-tools.js +395 -0
- package/dist/agents/dynamic-tools.js.map +1 -0
- package/dist/agents/index.d.ts +29 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +29 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/installer.d.ts +39 -0
- package/dist/agents/installer.d.ts.map +1 -0
- package/dist/agents/installer.js +205 -0
- package/dist/agents/installer.js.map +1 -0
- package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/{agterm → agents}/orchestrator.js +22 -2
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/sdk-backend.d.ts +63 -0
- package/dist/agents/sdk-backend.d.ts.map +1 -0
- package/dist/agents/sdk-backend.js +489 -0
- package/dist/agents/sdk-backend.js.map +1 -0
- package/dist/agents/swarm-types.d.ts +83 -0
- package/dist/agents/swarm-types.d.ts.map +1 -0
- package/dist/agents/swarm-types.js +20 -0
- package/dist/agents/swarm-types.js.map +1 -0
- package/dist/agents/swarm.d.ts +74 -0
- package/dist/agents/swarm.d.ts.map +1 -0
- package/dist/agents/swarm.js +307 -0
- package/dist/agents/swarm.js.map +1 -0
- package/dist/{agterm → agents}/tools.d.ts +7 -5
- package/dist/agents/tools.d.ts.map +1 -0
- package/dist/agents/tools.js +776 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/{agterm → agents}/types.d.ts +14 -2
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/{agterm → agents}/types.js +2 -2
- package/dist/agents/types.js.map +1 -0
- package/dist/api-server.d.ts +26 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +230 -0
- package/dist/api-server.js.map +1 -0
- package/dist/auto-checkpoint.d.ts +35 -0
- package/dist/auto-checkpoint.d.ts.map +1 -0
- package/dist/auto-checkpoint.js +143 -0
- package/dist/auto-checkpoint.js.map +1 -0
- package/dist/auto-compressor.d.ts +44 -0
- package/dist/auto-compressor.d.ts.map +1 -0
- package/dist/auto-compressor.js +145 -0
- package/dist/auto-compressor.js.map +1 -0
- package/dist/background-jobs.d.ts +45 -0
- package/dist/background-jobs.d.ts.map +1 -0
- package/dist/background-jobs.js +122 -0
- package/dist/background-jobs.js.map +1 -0
- package/dist/bin.d.ts +6 -2
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +127 -24
- package/dist/bin.js.map +1 -1
- package/dist/checkpoint.d.ts +49 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +219 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/circuit-breaker/breaker.d.ts +80 -0
- package/dist/circuit-breaker/breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/breaker.js +408 -0
- package/dist/circuit-breaker/breaker.js.map +1 -0
- package/dist/circuit-breaker/defaults.d.ts +8 -0
- package/dist/circuit-breaker/defaults.d.ts.map +1 -0
- package/dist/circuit-breaker/defaults.js +35 -0
- package/dist/circuit-breaker/defaults.js.map +1 -0
- package/dist/circuit-breaker/index.d.ts +9 -0
- package/dist/circuit-breaker/index.d.ts.map +1 -0
- package/dist/circuit-breaker/index.js +8 -0
- package/dist/circuit-breaker/index.js.map +1 -0
- package/dist/circuit-breaker/types.d.ts +77 -0
- package/dist/circuit-breaker/types.d.ts.map +1 -0
- package/dist/circuit-breaker/types.js +8 -0
- package/dist/circuit-breaker/types.js.map +1 -0
- package/dist/circuit-breaker.d.ts +8 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +7 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/cli/agent.d.ts +9 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +262 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/commands.d.ts +12 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/{cli.js → cli/commands.js} +285 -422
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +222 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +30 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +20 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/companions.d.ts +54 -0
- package/dist/companions.d.ts.map +1 -0
- package/dist/companions.js +440 -0
- package/dist/companions.js.map +1 -0
- package/dist/config.d.ts +23 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +95 -22
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +27 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +415 -10
- package/dist/diff.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -11
- package/dist/errors.js.map +1 -1
- package/dist/git-status.d.ts +23 -0
- package/dist/git-status.d.ts.map +1 -0
- package/dist/git-status.js +92 -0
- package/dist/git-status.js.map +1 -0
- package/dist/headless.d.ts +25 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +182 -0
- package/dist/headless.js.map +1 -0
- package/dist/hud/api.d.ts +35 -0
- package/dist/hud/api.d.ts.map +1 -0
- package/dist/hud/api.js +448 -0
- package/dist/hud/api.js.map +1 -0
- package/dist/hud/palettes.d.ts +9 -0
- package/dist/hud/palettes.d.ts.map +1 -0
- package/dist/hud/palettes.js +280 -0
- package/dist/hud/palettes.js.map +1 -0
- package/dist/hud/skins.d.ts +12 -0
- package/dist/hud/skins.d.ts.map +1 -0
- package/dist/hud/skins.js +365 -0
- package/dist/hud/skins.js.map +1 -0
- package/dist/hud/theme-packs/api.d.ts +51 -0
- package/dist/hud/theme-packs/api.d.ts.map +1 -0
- package/dist/hud/theme-packs/api.js +145 -0
- package/dist/hud/theme-packs/api.js.map +1 -0
- package/dist/hud/theme-packs/index.d.ts +18 -0
- package/dist/hud/theme-packs/index.d.ts.map +1 -0
- package/dist/hud/theme-packs/index.js +38 -0
- package/dist/hud/theme-packs/index.js.map +1 -0
- package/dist/hud/theme-packs/types.d.ts +29 -0
- package/dist/hud/theme-packs/types.d.ts.map +1 -0
- package/dist/hud/theme-packs/types.js +9 -0
- package/dist/hud/theme-packs/types.js.map +1 -0
- package/dist/hud/types.d.ts +182 -0
- package/dist/hud/types.d.ts.map +1 -0
- package/dist/hud/types.js +7 -0
- package/dist/hud/types.js.map +1 -0
- package/dist/idle-eviction.d.ts +34 -0
- package/dist/idle-eviction.d.ts.map +1 -0
- package/dist/idle-eviction.js +78 -0
- package/dist/idle-eviction.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/iteration-ledger.d.ts +105 -0
- package/dist/iteration-ledger.d.ts.map +1 -0
- package/dist/iteration-ledger.js +237 -0
- package/dist/iteration-ledger.js.map +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +1 -27
- package/dist/markdown.js.map +1 -1
- package/dist/mcp.d.ts +35 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +291 -7
- package/dist/mcp.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +12 -2
- package/dist/memory.js.map +1 -1
- package/dist/model-detection.d.ts +5 -0
- package/dist/model-detection.d.ts.map +1 -1
- package/dist/model-detection.js +278 -10
- package/dist/model-detection.js.map +1 -1
- package/dist/model-router.d.ts.map +1 -1
- package/dist/model-router.js +33 -11
- package/dist/model-router.js.map +1 -1
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +97 -6
- package/dist/plugins.js.map +1 -1
- package/dist/providers/anthropic.d.ts +10 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +221 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +17 -0
- package/dist/providers/bedrock.d.ts.map +1 -0
- package/dist/providers/bedrock.js +574 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/compat.d.ts +13 -0
- package/dist/providers/compat.d.ts.map +1 -0
- package/dist/providers/compat.js +202 -0
- package/dist/providers/compat.js.map +1 -0
- package/dist/providers/google.d.ts +10 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +203 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +23 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +145 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +17 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +289 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +121 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +485 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +63 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +164 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/sandbox-native.d.ts +59 -0
- package/dist/sandbox-native.d.ts.map +1 -0
- package/dist/sandbox-native.js +292 -0
- package/dist/sandbox-native.js.map +1 -0
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +59 -13
- package/dist/sandbox.js.map +1 -1
- package/dist/scope.d.ts +3 -1
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +13 -1
- package/dist/scope.js.map +1 -1
- package/dist/session-timeout.d.ts +31 -0
- package/dist/session-timeout.d.ts.map +1 -0
- package/dist/session-timeout.js +100 -0
- package/dist/session-timeout.js.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +29 -17
- package/dist/setup.js.map +1 -1
- package/dist/smart-router.d.ts +73 -0
- package/dist/smart-router.d.ts.map +1 -0
- package/dist/smart-router.js +332 -0
- package/dist/smart-router.js.map +1 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +164 -1
- package/dist/storage.js.map +1 -1
- package/dist/streaming.d.ts +4 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +12 -0
- package/dist/streaming.js.map +1 -1
- package/dist/styles.d.ts +32 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +91 -0
- package/dist/styles.js.map +1 -1
- package/dist/summarization.d.ts +1 -1
- package/dist/summarization.js +4 -4
- package/dist/summarization.js.map +1 -1
- package/dist/terminal-image.d.ts +115 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +766 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal-recording.d.ts +55 -0
- package/dist/terminal-recording.d.ts.map +1 -0
- package/dist/terminal-recording.js +182 -0
- package/dist/terminal-recording.js.map +1 -0
- package/dist/themes.d.ts +19 -35
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +101 -210
- package/dist/themes.js.map +1 -1
- package/dist/tmux.d.ts +35 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +106 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +587 -45
- package/dist/tools.js.map +1 -1
- package/dist/trust.d.ts +53 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +154 -0
- package/dist/trust.js.map +1 -0
- package/dist/types.d.ts +7 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +70 -32
- package/dist/types.js.map +1 -1
- package/dist/ui/agent.d.ts +61 -0
- package/dist/ui/agent.d.ts.map +1 -0
- package/dist/ui/agent.js +768 -0
- package/dist/ui/agent.js.map +1 -0
- package/dist/ui/chat-input.d.ts +32 -0
- package/dist/ui/chat-input.d.ts.map +1 -0
- package/dist/ui/chat-input.js +355 -0
- package/dist/ui/chat-input.js.map +1 -0
- package/dist/ui/commands.d.ts +92 -0
- package/dist/ui/commands.d.ts.map +1 -0
- package/dist/ui/commands.js +3006 -0
- package/dist/ui/commands.js.map +1 -0
- package/dist/ui/completions.d.ts +22 -0
- package/dist/ui/completions.d.ts.map +1 -0
- package/dist/ui/completions.js +215 -0
- package/dist/ui/completions.js.map +1 -0
- package/dist/ui/components.d.ts +38 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +422 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/context.d.ts +12 -0
- package/dist/ui/context.d.ts.map +1 -0
- package/dist/ui/context.js +102 -0
- package/dist/ui/context.js.map +1 -0
- package/dist/ui/error-boundary.d.ts +33 -0
- package/dist/ui/error-boundary.d.ts.map +1 -0
- package/dist/ui/error-boundary.js +94 -0
- package/dist/ui/error-boundary.js.map +1 -0
- package/dist/ui/frame.d.ts +13 -0
- package/dist/ui/frame.d.ts.map +1 -0
- package/dist/ui/frame.js +89 -0
- package/dist/ui/frame.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +928 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/messages.d.ts +19 -0
- package/dist/ui/messages.d.ts.map +1 -0
- package/dist/ui/messages.js +181 -0
- package/dist/ui/messages.js.map +1 -0
- package/dist/ui/modals.d.ts +52 -0
- package/dist/ui/modals.d.ts.map +1 -0
- package/dist/ui/modals.js +204 -0
- package/dist/ui/modals.js.map +1 -0
- package/dist/ui/pack-picker.d.ts +12 -0
- package/dist/ui/pack-picker.d.ts.map +1 -0
- package/dist/ui/pack-picker.js +101 -0
- package/dist/ui/pack-picker.js.map +1 -0
- package/dist/ui/status-bar.d.ts +20 -0
- package/dist/ui/status-bar.d.ts.map +1 -0
- package/dist/ui/status-bar.js +41 -0
- package/dist/ui/status-bar.js.map +1 -0
- package/dist/ui/theme-picker.d.ts +24 -0
- package/dist/ui/theme-picker.d.ts.map +1 -0
- package/dist/ui/theme-picker.js +190 -0
- package/dist/ui/theme-picker.js.map +1 -0
- package/dist/ui/types.d.ts +62 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +7 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -9
- package/dist/version-check.js.map +1 -1
- package/package.json +8 -3
- package/dist/agterm/agent-detection.js.map +0 -1
- package/dist/agterm/cli-backend.js.map +0 -1
- package/dist/agterm/index.d.ts +0 -12
- package/dist/agterm/index.d.ts.map +0 -1
- package/dist/agterm/index.js +0 -15
- package/dist/agterm/index.js.map +0 -1
- package/dist/agterm/orchestrator.d.ts.map +0 -1
- package/dist/agterm/orchestrator.js.map +0 -1
- package/dist/agterm/tools.d.ts.map +0 -1
- package/dist/agterm/tools.js +0 -278
- package/dist/agterm/tools.js.map +0 -1
- package/dist/agterm/types.d.ts.map +0 -1
- package/dist/agterm/types.js.map +0 -1
- package/dist/cli.d.ts +0 -14
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/providers.d.ts +0 -51
- package/dist/providers.d.ts.map +0 -1
- package/dist/providers.js +0 -1146
- package/dist/providers.js.map +0 -1
- package/dist/ui-cli.d.ts +0 -17
- package/dist/ui-cli.d.ts.map +0 -1
- package/dist/ui-cli.js +0 -3730
- package/dist/ui-cli.js.map +0 -1
package/dist/ui-cli.js
DELETED
|
@@ -1,3730 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Calliope CLI - Ink UI
|
|
4
|
-
*
|
|
5
|
-
* Component hierarchy inspired by Claude Code:
|
|
6
|
-
* App
|
|
7
|
-
* └── TerminalChat (main hub)
|
|
8
|
-
* ├── MessageHistory (Static for messages)
|
|
9
|
-
* │ └── MessageItem (formatted messages)
|
|
10
|
-
* ├── ProcessingIndicator (animated spinner)
|
|
11
|
-
* ├── ChatInput (input line)
|
|
12
|
-
* └── StatusBar (footer)
|
|
13
|
-
*/
|
|
14
|
-
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
15
|
-
import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
|
|
16
|
-
import * as fs from 'fs';
|
|
17
|
-
import * as path from 'path';
|
|
18
|
-
import * as config from './config.js';
|
|
19
|
-
import { chat, getAvailableProviders, selectProvider, needsSummarization, estimateContextUsage } from './providers.js';
|
|
20
|
-
import { executeTool, getTools } from './tools.js';
|
|
21
|
-
import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
|
|
22
|
-
import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
|
|
23
|
-
import { getAvailableModels, getModelContextLimit, preWarmModelCache } from './model-detection.js';
|
|
24
|
-
import { assessToolRisk, detectComplexity } from './risk.js';
|
|
25
|
-
import { formatError, classifyError } from './errors.js';
|
|
26
|
-
import * as storage from './storage.js';
|
|
27
|
-
import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
|
|
28
|
-
import { renderMarkdown } from './markdown.js';
|
|
29
|
-
import * as mcp from './mcp.js';
|
|
30
|
-
import * as skills from './skills.js';
|
|
31
|
-
import * as memory from './memory.js';
|
|
32
|
-
import * as hooks from './hooks.js';
|
|
33
|
-
import * as modelRouter from './model-router.js';
|
|
34
|
-
import * as summarization from './summarization.js';
|
|
35
|
-
import { requiresConfirmation } from './risk.js';
|
|
36
|
-
import { executeParallel, getParallelizationStats } from './parallel-tools.js';
|
|
37
|
-
import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetScope } from './scope.js';
|
|
38
|
-
import { getAgentStatusReport } from './agterm/index.js';
|
|
39
|
-
// Module-level state for agterm mode
|
|
40
|
-
let moduleAgtermEnabled = false;
|
|
41
|
-
// Debug logging for flow control issues
|
|
42
|
-
let debugEnabled = process.env.CALLIOPE_DEBUG === '1';
|
|
43
|
-
const debugLog = (label, ...args) => {
|
|
44
|
-
if (debugEnabled) {
|
|
45
|
-
const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
|
|
46
|
-
console.error(`[${timestamp}] ${label}:`, ...args);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Log error to persistent file for debugging
|
|
51
|
-
*/
|
|
52
|
-
function logErrorToFile(error, componentStack) {
|
|
53
|
-
try {
|
|
54
|
-
const errorLogPath = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.calliope-cli', 'errors.log');
|
|
55
|
-
const errorLogDir = path.dirname(errorLogPath);
|
|
56
|
-
// Ensure directory exists
|
|
57
|
-
if (!fs.existsSync(errorLogDir)) {
|
|
58
|
-
fs.mkdirSync(errorLogDir, { recursive: true });
|
|
59
|
-
}
|
|
60
|
-
const logEntry = {
|
|
61
|
-
timestamp: new Date().toISOString(),
|
|
62
|
-
error: error?.message || 'Unknown error',
|
|
63
|
-
stack: error?.stack || '',
|
|
64
|
-
componentStack,
|
|
65
|
-
nodeVersion: process.version,
|
|
66
|
-
platform: process.platform,
|
|
67
|
-
};
|
|
68
|
-
const logLine = JSON.stringify(logEntry) + '\n';
|
|
69
|
-
// Append to log file (create if doesn't exist)
|
|
70
|
-
fs.appendFileSync(errorLogPath, logLine, 'utf-8');
|
|
71
|
-
// Rotate log if too large (> 1MB)
|
|
72
|
-
const stats = fs.statSync(errorLogPath);
|
|
73
|
-
if (stats.size > 1024 * 1024) {
|
|
74
|
-
const backupPath = errorLogPath + '.old';
|
|
75
|
-
if (fs.existsSync(backupPath)) {
|
|
76
|
-
fs.unlinkSync(backupPath);
|
|
77
|
-
}
|
|
78
|
-
fs.renameSync(errorLogPath, backupPath);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// Silently fail if we can't write to log file
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
class ErrorBoundary extends React.Component {
|
|
86
|
-
constructor(props) {
|
|
87
|
-
super(props);
|
|
88
|
-
this.state = { hasError: false, error: null, errorInfo: '' };
|
|
89
|
-
}
|
|
90
|
-
static getDerivedStateFromError(error) {
|
|
91
|
-
return { hasError: true, error };
|
|
92
|
-
}
|
|
93
|
-
componentDidCatch(error, errorInfo) {
|
|
94
|
-
// Log error details
|
|
95
|
-
const info = errorInfo.componentStack || '';
|
|
96
|
-
this.setState({ errorInfo: info });
|
|
97
|
-
// Log to console
|
|
98
|
-
console.error('Calliope Error:', error);
|
|
99
|
-
console.error('Component Stack:', info);
|
|
100
|
-
// Log to persistent file for debugging
|
|
101
|
-
logErrorToFile(error, info);
|
|
102
|
-
}
|
|
103
|
-
handleRetry = () => {
|
|
104
|
-
this.setState({ hasError: false, error: null, errorInfo: '' });
|
|
105
|
-
this.props.onReset?.();
|
|
106
|
-
};
|
|
107
|
-
render() {
|
|
108
|
-
if (this.state.hasError) {
|
|
109
|
-
return _jsx(ErrorFallback, { error: this.state.error, errorInfo: this.state.errorInfo, onRetry: this.handleRetry });
|
|
110
|
-
}
|
|
111
|
-
return this.props.children;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function ErrorFallback({ error, errorInfo, onRetry }) {
|
|
115
|
-
const { exit } = useApp();
|
|
116
|
-
useInput((input, key) => {
|
|
117
|
-
if (input === 'r' || input === 'R') {
|
|
118
|
-
onRetry();
|
|
119
|
-
}
|
|
120
|
-
else if (input === 'q' || input === 'Q' || key.escape) {
|
|
121
|
-
exit();
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: "\u26A0\uFE0F Calliope encountered an error" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "red", padding: 1, children: [_jsx(Text, { color: "red", children: error?.message || 'Unknown error' }), error?.name && error.name !== 'Error' && (_jsxs(Text, { dimColor: true, children: ["Type: ", error.name] }))] }), errorInfo && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Component trace:" }), _jsx(Text, { dimColor: true, children: errorInfo.split('\n').slice(0, 5).join('\n') })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), _jsx(Text, { children: "etry " }), _jsx(Text, { color: "cyan", children: "[Q]" }), _jsx(Text, { children: "uit" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "If this persists, try: calliope --legacy" }) })] }));
|
|
125
|
-
}
|
|
126
|
-
// ============================================================================
|
|
127
|
-
// Constants
|
|
128
|
-
// ============================================================================
|
|
129
|
-
const BANNER_LINES = [
|
|
130
|
-
' ██████╗ █████╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗',
|
|
131
|
-
'██╔════╝██╔══██╗██║ ██║ ██║██╔═══██╗██╔══██╗██╔════╝',
|
|
132
|
-
'██║ ███████║██║ ██║ ██║██║ ██║██████╔╝█████╗ ',
|
|
133
|
-
'██║ ██╔══██║██║ ██║ ██║██║ ██║██╔═══╝ ██╔══╝ ',
|
|
134
|
-
'╚██████╗██║ ██║███████╗███████╗██║╚██████╔╝██║ ███████╗',
|
|
135
|
-
' ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚══════╝',
|
|
136
|
-
];
|
|
137
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
138
|
-
const TOOL_ICONS = {
|
|
139
|
-
shell: '⚡',
|
|
140
|
-
read_file: '📄',
|
|
141
|
-
write_file: '✍️',
|
|
142
|
-
list_files: '📁',
|
|
143
|
-
think: '💭',
|
|
144
|
-
execute_code: '▶️',
|
|
145
|
-
web_search: '🔍',
|
|
146
|
-
git: '🔀',
|
|
147
|
-
mermaid: '📊',
|
|
148
|
-
// AGTerm tools
|
|
149
|
-
spawn_agent: '🤖',
|
|
150
|
-
check_agent: '📋',
|
|
151
|
-
list_agents: '📊',
|
|
152
|
-
cancel_agent: '🛑',
|
|
153
|
-
};
|
|
154
|
-
// ============================================================================
|
|
155
|
-
// Utility Components
|
|
156
|
-
// ============================================================================
|
|
157
|
-
function Separator() {
|
|
158
|
-
const { stdout } = useStdout();
|
|
159
|
-
const width = stdout?.columns || 80;
|
|
160
|
-
return _jsx(Text, { dimColor: true, children: '─'.repeat(width) });
|
|
161
|
-
}
|
|
162
|
-
function ThinkingDisplay({ state }) {
|
|
163
|
-
const [frame, setFrame] = useState(0);
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
const timer = setInterval(() => {
|
|
166
|
-
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
167
|
-
}, 80);
|
|
168
|
-
return () => clearInterval(timer);
|
|
169
|
-
}, []);
|
|
170
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration != null && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
|
|
171
|
-
}
|
|
172
|
-
// Legacy simple indicator for non-agent operations
|
|
173
|
-
function ProcessingIndicator({ label }) {
|
|
174
|
-
const [frame, setFrame] = useState(0);
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
const timer = setInterval(() => {
|
|
177
|
-
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
178
|
-
}, 80);
|
|
179
|
-
return () => clearInterval(timer);
|
|
180
|
-
}, []);
|
|
181
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
182
|
-
}
|
|
183
|
-
// Indicator shown during streaming to show current activity
|
|
184
|
-
function StreamingIndicator({ activity }) {
|
|
185
|
-
const [frame, setFrame] = useState(0);
|
|
186
|
-
const [elapsed, setElapsed] = useState(0);
|
|
187
|
-
const pulseFrames = ['·', '•', '●', '•'];
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
const timer = setInterval(() => {
|
|
190
|
-
setFrame(f => (f + 1) % pulseFrames.length);
|
|
191
|
-
if (activity) {
|
|
192
|
-
setElapsed(Math.floor((Date.now() - activity.startTime) / 1000));
|
|
193
|
-
}
|
|
194
|
-
}, 200);
|
|
195
|
-
return () => clearInterval(timer);
|
|
196
|
-
}, [activity]);
|
|
197
|
-
if (activity) {
|
|
198
|
-
const elapsedStr = elapsed > 0 ? ` (${elapsed}s)` : '';
|
|
199
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: pulseFrames[frame] }), _jsxs(Text, { children: [" ", activity.action] }), activity.target && _jsxs(Text, { dimColor: true, children: [" ", activity.target] }), _jsx(Text, { dimColor: true, children: elapsedStr })] }) }));
|
|
200
|
-
}
|
|
201
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: pulseFrames[frame] }), _jsx(Text, { dimColor: true, children: " receiving..." })] }));
|
|
202
|
-
}
|
|
203
|
-
function MessageItem({ msg, collapse }) {
|
|
204
|
-
// Determine if this tool should be collapsed
|
|
205
|
-
const shouldCollapseThisTool = collapse?.collapseTools &&
|
|
206
|
-
collapse.toolDisplayLimit > 0 &&
|
|
207
|
-
collapse.toolIndex !== undefined &&
|
|
208
|
-
collapse.totalTools !== undefined &&
|
|
209
|
-
(collapse.totalTools - collapse.toolIndex) > collapse.toolDisplayLimit;
|
|
210
|
-
switch (msg.type) {
|
|
211
|
-
case 'user':
|
|
212
|
-
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u203A" }), " ", msg.content] }) }));
|
|
213
|
-
case 'assistant': {
|
|
214
|
-
// Render markdown with syntax highlighting
|
|
215
|
-
const rendered = renderMarkdown(msg.content);
|
|
216
|
-
// Collapse consecutive blank lines to single blank line
|
|
217
|
-
const lines = rendered.split('\n').reduce((acc, line, i, arr) => {
|
|
218
|
-
// Skip if this is a blank line following another blank line
|
|
219
|
-
if (line === '' && acc.length > 0 && acc[acc.length - 1] === '') {
|
|
220
|
-
return acc;
|
|
221
|
-
}
|
|
222
|
-
acc.push(line);
|
|
223
|
-
return acc;
|
|
224
|
-
}, []);
|
|
225
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i)))] }));
|
|
226
|
-
}
|
|
227
|
-
case 'tool': {
|
|
228
|
-
const isToolCall = msg.content.startsWith('⚡');
|
|
229
|
-
const isThinkTool = msg.content.includes('💭') || msg.content.startsWith('Perfect!') || msg.content.startsWith('Let me');
|
|
230
|
-
// Check if this is a think tool that should be collapsed
|
|
231
|
-
if (collapse?.collapseThinking && isThinkTool && !isToolCall) {
|
|
232
|
-
const preview = msg.content.substring(0, 50).replace(/\n/g, ' ');
|
|
233
|
-
return (_jsxs(Text, { dimColor: true, children: ["\u2570\u2500 \uD83D\uDCAD ", _jsxs(Text, { italic: true, children: [preview, "..."] })] }));
|
|
234
|
-
}
|
|
235
|
-
// Check if this tool should be collapsed (based on toolDisplayLimit)
|
|
236
|
-
if (shouldCollapseThisTool || (collapse?.collapseTools && !isToolCall)) {
|
|
237
|
-
// Show collapsed single-line version
|
|
238
|
-
const firstLine = msg.content.split('\n')[0].substring(0, 60);
|
|
239
|
-
return (_jsxs(Text, { dimColor: true, children: ["\u2570\u2500 \u25B8 ", firstLine, msg.content.length > 60 ? '...' : ''] }));
|
|
240
|
-
}
|
|
241
|
-
if (isToolCall) {
|
|
242
|
-
const match = msg.content.match(/^⚡ (\w+): (.*)$/);
|
|
243
|
-
if (match) {
|
|
244
|
-
const [, toolName, preview] = match;
|
|
245
|
-
const icon = TOOL_ICONS[toolName] || '⚙️';
|
|
246
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), " ", icon, " ", _jsx(Text, { color: "yellow", children: toolName })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: preview })] })] }));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Check for diff output from write_file
|
|
250
|
-
const isDiff = msg.content.startsWith('DIFF:');
|
|
251
|
-
if (isDiff) {
|
|
252
|
-
const lines = msg.content.split('\n');
|
|
253
|
-
const header = lines[0];
|
|
254
|
-
const isNewFile = header.includes('NEW_FILE:');
|
|
255
|
-
const filePath = isNewFile
|
|
256
|
-
? header.replace('DIFF:NEW_FILE:', '')
|
|
257
|
-
: header.replace('DIFF:', '');
|
|
258
|
-
// Find summary line (starts with ⎿)
|
|
259
|
-
const summaryLine = lines.find(l => l.startsWith('⎿'));
|
|
260
|
-
const diffStartIdx = summaryLine ? lines.indexOf(summaryLine) + 1 : 1;
|
|
261
|
-
const diffLines = lines.slice(diffStartIdx, diffStartIdx + 12);
|
|
262
|
-
const hasMore = lines.length > diffStartIdx + 12;
|
|
263
|
-
// Claude Code style diff display
|
|
264
|
-
const action = isNewFile ? 'Write' : 'Update';
|
|
265
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: action }), _jsx(Text, { dimColor: true, children: "(" }), _jsx(Text, { children: filePath }), _jsx(Text, { dimColor: true, children: ")" })] }), summaryLine && (_jsxs(Text, { children: [" ", _jsx(Text, { dimColor: true, children: summaryLine })] })), diffLines.map((line, i) => {
|
|
266
|
-
// Check for line number format: " 123 + content" or " 123 - content"
|
|
267
|
-
const lineNumMatch = line.match(/^(\s*\d+)\s*([+-])\s{2}(.*)$/);
|
|
268
|
-
if (lineNumMatch) {
|
|
269
|
-
const [, lineNum, prefix, content] = lineNumMatch;
|
|
270
|
-
const color = prefix === '+' ? 'green' : 'red';
|
|
271
|
-
return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [" ", lineNum] }), _jsxs(Text, { color: color, children: [" ", prefix] }), _jsxs(Text, { color: color, children: [" ", content.substring(0, 70)] })] }, i));
|
|
272
|
-
}
|
|
273
|
-
// Context line with line number: " 123 content"
|
|
274
|
-
const contextMatch = line.match(/^(\s*\d+)\s{4}(.*)$/);
|
|
275
|
-
if (contextMatch) {
|
|
276
|
-
const [, lineNum, content] = contextMatch;
|
|
277
|
-
return (_jsx(Text, { children: _jsxs(Text, { dimColor: true, children: [" ", lineNum, " ", content.substring(0, 70)] }) }, i));
|
|
278
|
-
}
|
|
279
|
-
// Fallback for old format or other lines
|
|
280
|
-
let color;
|
|
281
|
-
if (line.includes(' + ') || line.startsWith('+ '))
|
|
282
|
-
color = 'green';
|
|
283
|
-
else if (line.includes(' - ') || line.startsWith('- '))
|
|
284
|
-
color = 'red';
|
|
285
|
-
return (_jsx(Text, { children: _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] }) }, i));
|
|
286
|
-
}), hasMore && _jsx(Text, { dimColor: true, children: " ..." })] }));
|
|
287
|
-
}
|
|
288
|
-
// Regular tool result with enhanced status detection
|
|
289
|
-
const allLines = msg.content.split('\n');
|
|
290
|
-
const lines = allLines.slice(0, 5);
|
|
291
|
-
const totalLines = allLines.length;
|
|
292
|
-
const hasMore = totalLines > 5;
|
|
293
|
-
// Enhanced status detection
|
|
294
|
-
const lowerContent = msg.content.toLowerCase();
|
|
295
|
-
const hasError = lowerContent.includes('error') ||
|
|
296
|
-
lowerContent.includes('failed') ||
|
|
297
|
-
lowerContent.includes('permission denied') ||
|
|
298
|
-
lowerContent.includes('not found') ||
|
|
299
|
-
lowerContent.includes('exception');
|
|
300
|
-
const hasWarning = lowerContent.includes('warning') ||
|
|
301
|
-
lowerContent.includes('deprecated') ||
|
|
302
|
-
lowerContent.includes('caution');
|
|
303
|
-
// Determine status icon and color
|
|
304
|
-
let statusIcon = '✓';
|
|
305
|
-
let statusColor = 'green';
|
|
306
|
-
if (hasError) {
|
|
307
|
-
statusIcon = '✗';
|
|
308
|
-
statusColor = 'red';
|
|
309
|
-
}
|
|
310
|
-
else if (hasWarning) {
|
|
311
|
-
statusIcon = '⚠';
|
|
312
|
-
statusColor = 'yellow';
|
|
313
|
-
}
|
|
314
|
-
return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: line.substring(0, 100) })] }, i))), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsxs(Text, { dimColor: true, children: ["... (", totalLines - 5, " more lines)"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: statusColor, children: statusIcon })] })] }));
|
|
315
|
-
}
|
|
316
|
-
case 'system':
|
|
317
|
-
return _jsx(Text, { color: "yellow", children: msg.content });
|
|
318
|
-
case 'error':
|
|
319
|
-
return _jsxs(Text, { color: "red", children: ["\u2717 ", msg.content] });
|
|
320
|
-
default:
|
|
321
|
-
return _jsx(Text, { children: msg.content });
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
function MessageHistory({ messages, collapseSettings }) {
|
|
325
|
-
// Count tool messages for toolDisplayLimit calculation
|
|
326
|
-
const toolMessages = messages.filter(m => m.type === 'tool');
|
|
327
|
-
const totalTools = toolMessages.length;
|
|
328
|
-
// Track tool index
|
|
329
|
-
let toolIndex = 0;
|
|
330
|
-
return (_jsx(Static, { items: messages, children: (msg) => {
|
|
331
|
-
// For tool messages, pass index for collapse calculation
|
|
332
|
-
const msgCollapseSettings = msg.type === 'tool'
|
|
333
|
-
? { ...collapseSettings, toolIndex: toolIndex++, totalTools }
|
|
334
|
-
: collapseSettings;
|
|
335
|
-
return (_jsx(Box, { children: _jsx(MessageItem, { msg: msg, collapse: msgCollapseSettings }) }, msg.id));
|
|
336
|
-
} }));
|
|
337
|
-
}
|
|
338
|
-
// ============================================================================
|
|
339
|
-
// Modal Components
|
|
340
|
-
// ============================================================================
|
|
341
|
-
function ModelSelector({ models, onSelect, onCancel }) {
|
|
342
|
-
const [index, setIndex] = useState(0);
|
|
343
|
-
const pageSize = 10;
|
|
344
|
-
const start = Math.max(0, Math.min(index - Math.floor(pageSize / 2), models.length - pageSize));
|
|
345
|
-
const visible = models.slice(start, start + pageSize);
|
|
346
|
-
useInput((input, key) => {
|
|
347
|
-
if (key.upArrow)
|
|
348
|
-
setIndex(i => Math.max(0, i - 1));
|
|
349
|
-
else if (key.downArrow)
|
|
350
|
-
setIndex(i => Math.min(models.length - 1, i + 1));
|
|
351
|
-
else if (key.return)
|
|
352
|
-
onSelect(models[index].id);
|
|
353
|
-
else if (key.escape || input === 'q')
|
|
354
|
-
onCancel();
|
|
355
|
-
});
|
|
356
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "yellow", children: "Select model (\u2191/\u2193 navigate, Enter select, Esc cancel):" }), visible.map((model, i) => {
|
|
357
|
-
const globalIndex = start + i;
|
|
358
|
-
const isSelected = globalIndex === index;
|
|
359
|
-
const name = model.name || model.id;
|
|
360
|
-
const displayName = name.length > 50 ? name.slice(0, 47) + '...' : name;
|
|
361
|
-
return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', displayName] }, model.id));
|
|
362
|
-
}), models.length > pageSize && (_jsxs(Text, { dimColor: true, children: [" (", index + 1, "/", models.length, ")"] }))] }));
|
|
363
|
-
}
|
|
364
|
-
function SessionSelector({ sessions, onSelect, onDelete, onCancel }) {
|
|
365
|
-
const [index, setIndex] = useState(0);
|
|
366
|
-
const pageSize = 5;
|
|
367
|
-
// Keep selection visible - scroll window to follow selection
|
|
368
|
-
const start = Math.max(0, Math.min(index - pageSize + 1, sessions.length - pageSize));
|
|
369
|
-
const end = Math.min(start + pageSize, sessions.length);
|
|
370
|
-
const visible = sessions.slice(start, end);
|
|
371
|
-
const hasMore = sessions.length > pageSize;
|
|
372
|
-
const hasAbove = start > 0;
|
|
373
|
-
const hasBelow = end < sessions.length;
|
|
374
|
-
useInput((input, key) => {
|
|
375
|
-
if (key.upArrow)
|
|
376
|
-
setIndex(i => Math.max(0, i - 1));
|
|
377
|
-
else if (key.downArrow)
|
|
378
|
-
setIndex(i => Math.min(sessions.length - 1, i + 1));
|
|
379
|
-
else if (key.return && sessions.length > 0)
|
|
380
|
-
onSelect(sessions[index]);
|
|
381
|
-
else if ((key.backspace || key.delete) && sessions.length > 0)
|
|
382
|
-
onDelete(sessions[index]);
|
|
383
|
-
else if (key.escape || input === 'q')
|
|
384
|
-
onCancel();
|
|
385
|
-
});
|
|
386
|
-
const formatTimeAgo = (dateStr) => {
|
|
387
|
-
const diff = Date.now() - new Date(dateStr).getTime();
|
|
388
|
-
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
389
|
-
const days = Math.floor(hours / 24);
|
|
390
|
-
if (days > 0)
|
|
391
|
-
return `${days}d ago`;
|
|
392
|
-
if (hours > 0)
|
|
393
|
-
return `${hours}h ago`;
|
|
394
|
-
const minutes = Math.floor(diff / (1000 * 60));
|
|
395
|
-
return `${minutes}m ago`;
|
|
396
|
-
};
|
|
397
|
-
if (sessions.length === 0) {
|
|
398
|
-
return (_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { dimColor: true, children: "No sessions found. Press Esc to close." }) }));
|
|
399
|
-
}
|
|
400
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: "yellow", children: "Sessions (\u2191/\u2193, Enter load, Del delete, Esc cancel):" }), hasMore && hasAbove && _jsx(Text, { dimColor: true, children: " \u2191 more" }), visible.map((session, i) => {
|
|
401
|
-
const globalIndex = start + i;
|
|
402
|
-
const isSelected = globalIndex === index;
|
|
403
|
-
const timeAgo = formatTimeAgo(session.lastAccessedAt);
|
|
404
|
-
const name = session.projectName.length > 30 ? session.projectName.slice(0, 27) + '...' : session.projectName;
|
|
405
|
-
return (_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [isSelected ? '❯ ' : ' ', name, " ", _jsxs(Text, { dimColor: true, children: ["(", timeAgo, ", ", session.messageCount, " msgs)"] })] }, session.id));
|
|
406
|
-
}), hasMore && hasBelow && _jsx(Text, { dimColor: true, children: " \u2193 more" }), hasMore && _jsxs(Text, { dimColor: true, children: [" ", index + 1, "/", sessions.length] })] }));
|
|
407
|
-
}
|
|
408
|
-
function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
|
|
409
|
-
useInput((input, key) => {
|
|
410
|
-
if (input === 'y' || input === 'Y')
|
|
411
|
-
onConfirm();
|
|
412
|
-
else if (input === 'n' || input === 'N' || key.escape)
|
|
413
|
-
onCancel();
|
|
414
|
-
});
|
|
415
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: "yellow", children: ["Update available: v", currentVersion, " \u2192 ", _jsxs(Text, { color: "green", children: ["v", latestVersion] })] }), _jsxs(Text, { children: ["Upgrade now? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
|
|
416
|
-
}
|
|
417
|
-
function ComplexityWarning({ reason, prompt, onProceed, onPlan, onCancel, }) {
|
|
418
|
-
useInput((input, key) => {
|
|
419
|
-
if (input === 'p' || input === 'P')
|
|
420
|
-
onProceed();
|
|
421
|
-
else if (input === 'l' || input === 'L')
|
|
422
|
-
onPlan();
|
|
423
|
-
else if (key.escape || input === 'c' || input === 'C')
|
|
424
|
-
onCancel();
|
|
425
|
-
});
|
|
426
|
-
// Analyze the prompt for operation preview
|
|
427
|
-
const analysis = React.useMemo(() => {
|
|
428
|
-
if (!prompt)
|
|
429
|
-
return null;
|
|
430
|
-
const lower = prompt.toLowerCase();
|
|
431
|
-
const cwd = process.cwd();
|
|
432
|
-
// Parse file references
|
|
433
|
-
const fileRefs = parseFileReferences(prompt, cwd);
|
|
434
|
-
// Detect operation types
|
|
435
|
-
const operations = [];
|
|
436
|
-
if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) {
|
|
437
|
-
operations.push('Delete files');
|
|
438
|
-
}
|
|
439
|
-
if (lower.includes('create') || lower.includes('add') || lower.includes('new ')) {
|
|
440
|
-
operations.push('Create files');
|
|
441
|
-
}
|
|
442
|
-
if (lower.includes('modify') || lower.includes('change') || lower.includes('update') || lower.includes('edit')) {
|
|
443
|
-
operations.push('Modify files');
|
|
444
|
-
}
|
|
445
|
-
if (lower.includes('refactor') || lower.includes('restructure') || lower.includes('reorganize')) {
|
|
446
|
-
operations.push('Refactor code');
|
|
447
|
-
}
|
|
448
|
-
if (lower.includes('install') || lower.includes('npm') || lower.includes('yarn') || lower.includes('pip')) {
|
|
449
|
-
operations.push('Install packages');
|
|
450
|
-
}
|
|
451
|
-
if (lower.includes('git ') || lower.includes('commit') || lower.includes('push') || lower.includes('merge')) {
|
|
452
|
-
operations.push('Git operations');
|
|
453
|
-
}
|
|
454
|
-
if (lower.includes('test') || lower.includes('build') || lower.includes('compile')) {
|
|
455
|
-
operations.push('Build/Test');
|
|
456
|
-
}
|
|
457
|
-
// Estimate risk level based on keywords
|
|
458
|
-
let riskLevel = 'medium';
|
|
459
|
-
if (lower.includes('delete') || lower.includes('remove') || lower.includes('force') || lower.includes('--hard')) {
|
|
460
|
-
riskLevel = 'high';
|
|
461
|
-
}
|
|
462
|
-
else if (lower.includes('read') || lower.includes('show') || lower.includes('list') || lower.includes('find')) {
|
|
463
|
-
riskLevel = 'low';
|
|
464
|
-
}
|
|
465
|
-
return {
|
|
466
|
-
files: fileRefs.files,
|
|
467
|
-
operations,
|
|
468
|
-
riskLevel,
|
|
469
|
-
};
|
|
470
|
-
}, [prompt]);
|
|
471
|
-
const riskColors = { low: 'green', medium: 'yellow', high: 'red' };
|
|
472
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\uD83D\uDD0D Operation Preview" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: reason }), analysis && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), analysis.operations.length > 0 && (_jsxs(Text, { children: ["Operations: ", _jsx(Text, { color: "cyan", children: analysis.operations.join(', ') })] })), analysis.files.length > 0 && (_jsxs(Text, { children: ["Files referenced: ", _jsx(Text, { color: "cyan", children: analysis.files.length }), analysis.files.length <= 3 && (_jsxs(Text, { dimColor: true, children: [" (", analysis.files.map(f => f.split('/').pop()).join(', '), ")"] }))] })), _jsxs(Text, { children: ["Risk level: ", _jsx(Text, { color: riskColors[analysis.riskLevel], children: analysis.riskLevel.toUpperCase() })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { children: "This operation may affect multiple files or require careful planning." }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "How would you like to proceed?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "[P]" }), _jsx(Text, { children: "roceed directly " }), _jsx(Text, { color: "yellow", children: "[L]" }), _jsx(Text, { children: "et me plan first " }), _jsx(Text, { color: "red", children: "[C]" }), _jsx(Text, { children: "ancel" })] })] }));
|
|
473
|
-
}
|
|
474
|
-
function SessionResumePrompt({ session, onResume, onNew, }) {
|
|
475
|
-
useInput((input, key) => {
|
|
476
|
-
if (input === 'r' || input === 'R')
|
|
477
|
-
onResume();
|
|
478
|
-
else if (input === 'n' || input === 'N' || key.escape)
|
|
479
|
-
onNew();
|
|
480
|
-
});
|
|
481
|
-
const timeAgo = (() => {
|
|
482
|
-
const diff = Date.now() - new Date(session.lastAccessedAt).getTime();
|
|
483
|
-
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
484
|
-
const days = Math.floor(hours / 24);
|
|
485
|
-
if (days > 0)
|
|
486
|
-
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
487
|
-
if (hours > 0)
|
|
488
|
-
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
489
|
-
const minutes = Math.floor(diff / (1000 * 60));
|
|
490
|
-
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
491
|
-
})();
|
|
492
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\uD83D\uDCC2 Previous Session Found" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Project: ", _jsx(Text, { color: "yellow", children: session.projectName })] }), _jsxs(Text, { children: ["Last active: ", _jsx(Text, { dimColor: true, children: timeAgo })] }), _jsxs(Text, { children: ["Messages: ", _jsx(Text, { dimColor: true, children: session.messageCount })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "[R]" }), "esume session ", _jsx(Text, { color: "cyan", children: "[N]" }), "ew session"] })] }));
|
|
493
|
-
}
|
|
494
|
-
function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
|
|
495
|
-
useInput((input, key) => {
|
|
496
|
-
if (input === 'y' || input === 'Y')
|
|
497
|
-
onConfirm();
|
|
498
|
-
else if (input === 'n' || input === 'N' || key.escape)
|
|
499
|
-
onDeny();
|
|
500
|
-
});
|
|
501
|
-
const args = toolCall.arguments;
|
|
502
|
-
const preview = String(args.command || args.path || args.operation || '...');
|
|
503
|
-
const riskColor = riskLevel === 'critical' ? 'red' : 'yellow';
|
|
504
|
-
const riskIcon = riskLevel === 'critical' ? '⚠️' : '⚡';
|
|
505
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: riskColor, paddingX: 1, children: [_jsxs(Text, { color: riskColor, bold: true, children: [riskIcon, " ", riskLevel.toUpperCase(), " RISK OPERATION"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Tool: ", _jsx(Text, { color: "cyan", children: toolCall.name })] }), _jsxs(Text, { children: ["Command: ", _jsx(Text, { dimColor: true, children: preview.substring(0, 60) })] }), _jsxs(Text, { children: ["Reason: ", _jsx(Text, { dimColor: true, children: reason })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Execute this operation? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
|
|
506
|
-
}
|
|
507
|
-
// Keybindings modal component
|
|
508
|
-
function KeybindingsModal({ onClose }) {
|
|
509
|
-
useInput((input, key) => {
|
|
510
|
-
if (key.escape || key.return || input === 'q')
|
|
511
|
-
onClose();
|
|
512
|
-
});
|
|
513
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2328\uFE0F Keyboard Shortcuts" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "General:" }), _jsx(Text, { children: " Enter Submit message" }), _jsx(Text, { children: " Alt/Ctrl+Enter Insert newline (multiline)" }), _jsx(Text, { children: " Shift+Tab Cycle modes (plan/hybrid/work)" }), _jsx(Text, { children: " Esc Cancel operation / show hint" }), _jsx(Text, { children: " Ctrl+C Exit" }), _jsx(Text, { children: " \u2191/\u2193 Navigate input history" }), _jsx(Text, { children: " Tab Auto-complete commands/paths" }), _jsx(Text, { children: " Ctrl+U Clear input line" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "During Processing (queue mode):" }), _jsx(Text, { children: " Enter Queue message for later" }), _jsx(Text, { children: " !message Send directly (interrupt agent)" }), _jsx(Text, { children: " \u2191/\u2193 Edit queued messages" }), _jsx(Text, { children: " Ctrl+D Delete queued message" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "yellow", children: "Quick Commands:" }), _jsx(Text, { children: " /keys This help" }), _jsx(Text, { children: " /work Switch to work mode" }), _jsx(Text, { children: " /plan Switch to plan mode" }), _jsx(Text, { children: " /flush Force-process queue" }), _jsx(Text, { children: " /unstick Reset stuck state" }), _jsx(Text, { children: " /debug on/off Toggle debug mode" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press any key to close..." })] }));
|
|
514
|
-
}
|
|
515
|
-
// ============================================================================
|
|
516
|
-
// Slash Commands (for tab completion)
|
|
517
|
-
// ============================================================================
|
|
518
|
-
const SLASH_COMMANDS = [
|
|
519
|
-
'/help', '/h',
|
|
520
|
-
'/mode', '/m',
|
|
521
|
-
'/provider', '/p',
|
|
522
|
-
'/model',
|
|
523
|
-
'/models',
|
|
524
|
-
'/route',
|
|
525
|
-
'/persona',
|
|
526
|
-
'/todo',
|
|
527
|
-
'/plans',
|
|
528
|
-
'/session',
|
|
529
|
-
'/sessions',
|
|
530
|
-
'/history',
|
|
531
|
-
'/context',
|
|
532
|
-
'/summarize',
|
|
533
|
-
'/clear', '/c',
|
|
534
|
-
'/copy',
|
|
535
|
-
'/export',
|
|
536
|
-
'/edit',
|
|
537
|
-
'/undo',
|
|
538
|
-
'/redo',
|
|
539
|
-
'/confirm',
|
|
540
|
-
'/profile',
|
|
541
|
-
'/mcp',
|
|
542
|
-
'/skills',
|
|
543
|
-
'/memory',
|
|
544
|
-
'/project',
|
|
545
|
-
'/find',
|
|
546
|
-
'/branch',
|
|
547
|
-
'/theme',
|
|
548
|
-
'/hooks',
|
|
549
|
-
'/search',
|
|
550
|
-
'/status', '/s',
|
|
551
|
-
'/config',
|
|
552
|
-
'/set',
|
|
553
|
-
'/layout',
|
|
554
|
-
'/density',
|
|
555
|
-
'/collapse',
|
|
556
|
-
'/scope',
|
|
557
|
-
'/add-dir',
|
|
558
|
-
'/remove-dir',
|
|
559
|
-
'/agents',
|
|
560
|
-
'/upgrade',
|
|
561
|
-
'/loop',
|
|
562
|
-
'/cancel-loop',
|
|
563
|
-
'/exit',
|
|
564
|
-
'/keys',
|
|
565
|
-
'/?',
|
|
566
|
-
'/queue',
|
|
567
|
-
'/flush',
|
|
568
|
-
'/debug',
|
|
569
|
-
'/unstick',
|
|
570
|
-
'/work',
|
|
571
|
-
'/plan',
|
|
572
|
-
'/resume',
|
|
573
|
-
];
|
|
574
|
-
// Commands that take a path argument (for file tab completion)
|
|
575
|
-
const PATH_COMMANDS = ['/add-dir', '/remove-dir', '/export', '/find'];
|
|
576
|
-
/**
|
|
577
|
-
* Get file/directory completions for a partial path
|
|
578
|
-
*/
|
|
579
|
-
function getPathCompletions(partial, cwd) {
|
|
580
|
-
try {
|
|
581
|
-
// fs and path are imported at the top of the file
|
|
582
|
-
// Handle empty or relative paths
|
|
583
|
-
let searchDir;
|
|
584
|
-
let prefix;
|
|
585
|
-
if (!partial || partial === '') {
|
|
586
|
-
searchDir = cwd;
|
|
587
|
-
prefix = '';
|
|
588
|
-
}
|
|
589
|
-
else if (partial.startsWith('/')) {
|
|
590
|
-
// Absolute path
|
|
591
|
-
const lastSlash = partial.lastIndexOf('/');
|
|
592
|
-
searchDir = partial.substring(0, lastSlash) || '/';
|
|
593
|
-
prefix = partial.substring(lastSlash + 1);
|
|
594
|
-
}
|
|
595
|
-
else if (partial.startsWith('~')) {
|
|
596
|
-
// Home directory
|
|
597
|
-
const home = process.env.HOME || '/tmp';
|
|
598
|
-
const expanded = partial.replace('~', home);
|
|
599
|
-
const lastSlash = expanded.lastIndexOf('/');
|
|
600
|
-
searchDir = expanded.substring(0, lastSlash) || home;
|
|
601
|
-
prefix = expanded.substring(lastSlash + 1);
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
// Relative path
|
|
605
|
-
const lastSlash = partial.lastIndexOf('/');
|
|
606
|
-
if (lastSlash === -1) {
|
|
607
|
-
searchDir = cwd;
|
|
608
|
-
prefix = partial;
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
searchDir = path.join(cwd, partial.substring(0, lastSlash));
|
|
612
|
-
prefix = partial.substring(lastSlash + 1);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (!fs.existsSync(searchDir))
|
|
616
|
-
return [];
|
|
617
|
-
const entries = fs.readdirSync(searchDir, { withFileTypes: true });
|
|
618
|
-
const matches = [];
|
|
619
|
-
for (const entry of entries) {
|
|
620
|
-
if (entry.name.startsWith('.') && !prefix.startsWith('.'))
|
|
621
|
-
continue; // Skip hidden unless typing hidden
|
|
622
|
-
if (prefix && !entry.name.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
623
|
-
continue;
|
|
624
|
-
const fullPath = path.join(searchDir, entry.name);
|
|
625
|
-
const displayPath = partial.startsWith('/')
|
|
626
|
-
? fullPath
|
|
627
|
-
: partial.startsWith('~')
|
|
628
|
-
? fullPath.replace(process.env.HOME || '', '~')
|
|
629
|
-
: path.relative(cwd, fullPath) || entry.name;
|
|
630
|
-
matches.push(entry.isDirectory() ? displayPath + '/' : displayPath);
|
|
631
|
-
}
|
|
632
|
-
return matches.sort().slice(0, 10); // Limit to 10 suggestions
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
return [];
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
// ============================================================================
|
|
639
|
-
// Input Components
|
|
640
|
-
// ============================================================================
|
|
641
|
-
function ChatInput({ value, onChange, onSubmit, onEscape, onCycleMode, disabled, isProcessing, queuedCount, queuedMessages, editingQueueIndex, onQueueMessage, onEditQueuedMessage, onSetEditingQueueIndex, onDirectSend, cwd, suggestions, onSuggestionsChange, onNavigateHistory,
|
|
642
|
-
// Smart suggestion context
|
|
643
|
-
currentMode, contextPercentage, recentCommands, hasGitRepo, }) {
|
|
644
|
-
const workingDir = cwd || process.cwd();
|
|
645
|
-
// Debug logging (set CALLIOPE_DEBUG=1 to enable) - use async to avoid input lag
|
|
646
|
-
const debug = process.env.CALLIOPE_DEBUG === '1';
|
|
647
|
-
const log = debug
|
|
648
|
-
? (msg) => fs.appendFile('/tmp/calliope-debug.log', `${new Date().toISOString()} [input] ${msg}\n`, () => { })
|
|
649
|
-
: () => { };
|
|
650
|
-
// CRITICAL FIX: Use refs to track the current value and cursor position
|
|
651
|
-
// This prevents stale closure issues when typing rapidly before React re-renders
|
|
652
|
-
const valueRef = React.useRef(value);
|
|
653
|
-
const cursorRef = React.useRef(value.length); // Cursor position (0 = start, length = end)
|
|
654
|
-
const internalChangeRef = React.useRef(false); // Track if change was from typing
|
|
655
|
-
// Sync refs when prop changes (from external sources like history navigation)
|
|
656
|
-
React.useEffect(() => {
|
|
657
|
-
// Only reset cursor if change was external (not from our own typing)
|
|
658
|
-
if (!internalChangeRef.current) {
|
|
659
|
-
valueRef.current = value;
|
|
660
|
-
cursorRef.current = value.length; // Move cursor to end on external change
|
|
661
|
-
}
|
|
662
|
-
internalChangeRef.current = false;
|
|
663
|
-
}, [value]);
|
|
664
|
-
// Helper to update value - updates ref IMMEDIATELY, then notifies parent
|
|
665
|
-
const updateValue = (newValue, newCursor) => {
|
|
666
|
-
valueRef.current = newValue; // Update ref synchronously
|
|
667
|
-
cursorRef.current = newCursor ?? newValue.length; // Default cursor to end
|
|
668
|
-
internalChangeRef.current = true; // Mark as internal change
|
|
669
|
-
onChange(newValue); // Then notify parent (may batch)
|
|
670
|
-
};
|
|
671
|
-
// Force re-render for cursor position changes (cursor is visual only)
|
|
672
|
-
const [, forceRender] = React.useState(0);
|
|
673
|
-
const updateCursor = (pos) => {
|
|
674
|
-
cursorRef.current = Math.max(0, Math.min(pos, valueRef.current.length));
|
|
675
|
-
forceRender(n => n + 1);
|
|
676
|
-
};
|
|
677
|
-
// Handle ALL keyboard input here - single source of input handling
|
|
678
|
-
useInput((input, key) => {
|
|
679
|
-
const currentValue = valueRef.current;
|
|
680
|
-
log(`key: "${input}" ${JSON.stringify(key)} val="${currentValue}" disabled=${disabled}`);
|
|
681
|
-
// ESC to exit (always works)
|
|
682
|
-
if (key.escape) {
|
|
683
|
-
log('-> escape');
|
|
684
|
-
onEscape();
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
// Ctrl+C to exit (always works)
|
|
688
|
-
if (key.ctrl && input === 'c') {
|
|
689
|
-
onEscape();
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
// When fully disabled (modal), ignore all input
|
|
693
|
-
if (disabled) {
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
// When processing, queue messages instead of submitting directly
|
|
697
|
-
if (isProcessing) {
|
|
698
|
-
// Ensure cursor is valid
|
|
699
|
-
let cursor = cursorRef.current;
|
|
700
|
-
if (cursor > currentValue.length)
|
|
701
|
-
cursor = currentValue.length;
|
|
702
|
-
if (cursor < 0)
|
|
703
|
-
cursor = 0;
|
|
704
|
-
// Left/right arrow for cursor movement
|
|
705
|
-
if (key.leftArrow) {
|
|
706
|
-
updateCursor(cursor - 1);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
if (key.rightArrow) {
|
|
710
|
-
updateCursor(cursor + 1);
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
// Backspace - support multiple variants including Mac delete key
|
|
714
|
-
const isBackspace = key.backspace || key.delete || (key.ctrl && input === 'h') || input === '\x7f' || input === '\b';
|
|
715
|
-
if (isBackspace) {
|
|
716
|
-
if (cursor > 0) {
|
|
717
|
-
const newValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
|
|
718
|
-
updateValue(newValue, cursor - 1);
|
|
719
|
-
}
|
|
720
|
-
else if (currentValue.length > 0) {
|
|
721
|
-
updateValue(currentValue.slice(0, -1), currentValue.length - 1);
|
|
722
|
-
}
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (key.ctrl && input === 'u') {
|
|
726
|
-
updateValue('');
|
|
727
|
-
onSetEditingQueueIndex?.(null); // Clear editing state
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
// Ctrl+A to go to start, Ctrl+E to go to end
|
|
731
|
-
if (key.ctrl && input === 'a') {
|
|
732
|
-
updateCursor(0);
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (key.ctrl && input === 'e') {
|
|
736
|
-
updateCursor(currentValue.length);
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
// Up/Down arrows to navigate queued messages for editing
|
|
740
|
-
if (key.upArrow && queuedMessages && queuedMessages.length > 0) {
|
|
741
|
-
if (editingQueueIndex === null || editingQueueIndex === undefined) {
|
|
742
|
-
// Start editing the last queued message
|
|
743
|
-
const idx = queuedMessages.length - 1;
|
|
744
|
-
onSetEditingQueueIndex?.(idx);
|
|
745
|
-
updateValue(queuedMessages[idx]);
|
|
746
|
-
}
|
|
747
|
-
else if (editingQueueIndex > 0) {
|
|
748
|
-
// Move to previous message
|
|
749
|
-
const idx = editingQueueIndex - 1;
|
|
750
|
-
onSetEditingQueueIndex?.(idx);
|
|
751
|
-
updateValue(queuedMessages[idx]);
|
|
752
|
-
}
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
if (key.downArrow && queuedMessages && editingQueueIndex !== null && editingQueueIndex !== undefined) {
|
|
756
|
-
if (editingQueueIndex < queuedMessages.length - 1) {
|
|
757
|
-
// Move to next message
|
|
758
|
-
const idx = editingQueueIndex + 1;
|
|
759
|
-
onSetEditingQueueIndex?.(idx);
|
|
760
|
-
updateValue(queuedMessages[idx]);
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
// At the end, clear to new input
|
|
764
|
-
onSetEditingQueueIndex?.(null);
|
|
765
|
-
updateValue('');
|
|
766
|
-
}
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
// Alt+Enter or Ctrl+Enter to insert newline (multiline input)
|
|
770
|
-
if (key.return && (key.meta || key.ctrl)) {
|
|
771
|
-
updateValue(currentValue + '\n');
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
// Shift+Enter sends directly (interrupts current operation)
|
|
775
|
-
// Note: Many terminals don't distinguish Shift+Enter from Enter
|
|
776
|
-
// Use ! prefix as reliable alternative: "!message" sends immediately
|
|
777
|
-
if (key.return && key.shift && currentValue.trim() && onDirectSend) {
|
|
778
|
-
onDirectSend(currentValue.trim());
|
|
779
|
-
onSetEditingQueueIndex?.(null);
|
|
780
|
-
updateValue('');
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
// ! prefix sends directly: "!fix this now" interrupts and sends
|
|
784
|
-
if (key.return && currentValue.trim().startsWith('!') && onDirectSend) {
|
|
785
|
-
const msg = currentValue.trim().slice(1).trim(); // Remove ! prefix
|
|
786
|
-
if (msg) {
|
|
787
|
-
onDirectSend(msg);
|
|
788
|
-
onSetEditingQueueIndex?.(null);
|
|
789
|
-
updateValue('');
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
// Enter queues or updates the message
|
|
794
|
-
if (key.return && currentValue.trim()) {
|
|
795
|
-
if (editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
|
|
796
|
-
// Update existing queued message
|
|
797
|
-
onEditQueuedMessage(editingQueueIndex, currentValue.trim());
|
|
798
|
-
onSetEditingQueueIndex?.(null);
|
|
799
|
-
updateValue('');
|
|
800
|
-
}
|
|
801
|
-
else if (onQueueMessage) {
|
|
802
|
-
// Add new queued message
|
|
803
|
-
onQueueMessage(currentValue.trim());
|
|
804
|
-
updateValue('');
|
|
805
|
-
}
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
// Ctrl+D to delete currently editing queued message
|
|
809
|
-
if (key.ctrl && input === 'd' && editingQueueIndex !== null && editingQueueIndex !== undefined && onEditQueuedMessage) {
|
|
810
|
-
onEditQueuedMessage(editingQueueIndex, ''); // Empty string signals deletion
|
|
811
|
-
onSetEditingQueueIndex?.(null);
|
|
812
|
-
updateValue('');
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
// Regular input - insert at cursor position
|
|
816
|
-
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
817
|
-
const cursor = cursorRef.current;
|
|
818
|
-
const newValue = currentValue.slice(0, cursor) + input + currentValue.slice(cursor);
|
|
819
|
-
updateValue(newValue, cursor + input.length);
|
|
820
|
-
}
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
// Shift+Tab to cycle mode
|
|
824
|
-
if (key.shift && key.tab) {
|
|
825
|
-
onCycleMode();
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
// Alt+Enter or Ctrl+Enter to insert newline (multiline input)
|
|
829
|
-
if (key.return && (key.meta || key.ctrl)) {
|
|
830
|
-
updateValue(currentValue + '\n');
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
// Enter to submit
|
|
834
|
-
if (key.return) {
|
|
835
|
-
if (currentValue.trim()) {
|
|
836
|
-
onSubmit(currentValue);
|
|
837
|
-
}
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
// Cursor movement with arrow keys
|
|
841
|
-
// Ensure cursor is valid (might be out of sync)
|
|
842
|
-
let cursor = cursorRef.current;
|
|
843
|
-
if (cursor > currentValue.length)
|
|
844
|
-
cursor = currentValue.length;
|
|
845
|
-
if (cursor < 0)
|
|
846
|
-
cursor = 0;
|
|
847
|
-
if (key.leftArrow) {
|
|
848
|
-
updateCursor(cursor - 1);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
if (key.rightArrow) {
|
|
852
|
-
updateCursor(cursor + 1);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
// Backspace deletes character before cursor (or from end if cursor is 0 but there's text)
|
|
856
|
-
// Support multiple backspace variants:
|
|
857
|
-
// - key.backspace (Ink's detection)
|
|
858
|
-
// - key.delete (Mac delete key is often detected as this)
|
|
859
|
-
// - Ctrl+H (ASCII backspace control code)
|
|
860
|
-
// - \x7f DEL char (Mac delete key raw)
|
|
861
|
-
// - \b BS char (traditional backspace)
|
|
862
|
-
const isBackspace = key.backspace || key.delete || (key.ctrl && input === 'h') || input === '\x7f' || input === '\b';
|
|
863
|
-
if (isBackspace) {
|
|
864
|
-
if (cursor > 0) {
|
|
865
|
-
const newValue = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
|
|
866
|
-
updateValue(newValue, cursor - 1);
|
|
867
|
-
}
|
|
868
|
-
else if (currentValue.length > 0) {
|
|
869
|
-
// Fallback: delete from end if cursor is somehow at 0
|
|
870
|
-
updateValue(currentValue.slice(0, -1), currentValue.length - 1);
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
// Ctrl+U to clear line
|
|
875
|
-
if (key.ctrl && input === 'u') {
|
|
876
|
-
updateValue('');
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
// Ctrl+A to go to start, Ctrl+E to go to end
|
|
880
|
-
if (key.ctrl && input === 'a') {
|
|
881
|
-
updateCursor(0);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
if (key.ctrl && input === 'e') {
|
|
885
|
-
updateCursor(currentValue.length);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
// Tab completion for slash commands and paths
|
|
889
|
-
if (key.tab && !key.shift) {
|
|
890
|
-
// Check if we're completing a path after a path command
|
|
891
|
-
const parts = currentValue.split(/\s+/);
|
|
892
|
-
const cmd = parts[0]?.toLowerCase();
|
|
893
|
-
if (PATH_COMMANDS.includes(cmd) && parts.length >= 1) {
|
|
894
|
-
// Path completion
|
|
895
|
-
const pathPart = parts.slice(1).join(' ');
|
|
896
|
-
const completions = getPathCompletions(pathPart, workingDir);
|
|
897
|
-
if (completions.length === 1) {
|
|
898
|
-
updateValue(`${cmd} ${completions[0]}`);
|
|
899
|
-
onSuggestionsChange?.([]);
|
|
900
|
-
}
|
|
901
|
-
else if (completions.length > 1) {
|
|
902
|
-
// Find common prefix
|
|
903
|
-
let commonPrefix = completions[0];
|
|
904
|
-
for (const comp of completions) {
|
|
905
|
-
while (!comp.startsWith(commonPrefix)) {
|
|
906
|
-
commonPrefix = commonPrefix.slice(0, -1);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (commonPrefix.length > pathPart.length) {
|
|
910
|
-
updateValue(`${cmd} ${commonPrefix}`);
|
|
911
|
-
}
|
|
912
|
-
onSuggestionsChange?.(completions);
|
|
913
|
-
}
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
// Slash command completion with smart suggestions
|
|
917
|
-
if (currentValue.startsWith('/')) {
|
|
918
|
-
// Use smart suggestions if context is available
|
|
919
|
-
const smartMatches = getSmartCommandSuggestions({
|
|
920
|
-
input: currentValue,
|
|
921
|
-
hasGitRepo: hasGitRepo ?? false,
|
|
922
|
-
contextPercentage: contextPercentage ?? 0,
|
|
923
|
-
currentMode: currentMode ?? 'hybrid',
|
|
924
|
-
recentCommands: recentCommands ?? [],
|
|
925
|
-
isProcessing: isProcessing ?? false,
|
|
926
|
-
});
|
|
927
|
-
// Fall back to basic matching if smart suggestions didn't find anything
|
|
928
|
-
const partial = currentValue.toLowerCase();
|
|
929
|
-
const matches = smartMatches.length > 0 ? smartMatches : SLASH_COMMANDS.filter(cmdName => cmdName.startsWith(partial) && cmdName !== partial);
|
|
930
|
-
if (matches.length === 1) {
|
|
931
|
-
updateValue(matches[0] + ' ');
|
|
932
|
-
onSuggestionsChange?.([]);
|
|
933
|
-
}
|
|
934
|
-
else if (matches.length > 1) {
|
|
935
|
-
let commonPrefix = matches[0];
|
|
936
|
-
for (const match of matches) {
|
|
937
|
-
while (!match.startsWith(commonPrefix)) {
|
|
938
|
-
commonPrefix = commonPrefix.slice(0, -1);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if (commonPrefix.length > currentValue.length) {
|
|
942
|
-
updateValue(commonPrefix);
|
|
943
|
-
}
|
|
944
|
-
onSuggestionsChange?.(matches);
|
|
945
|
-
}
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Up/down arrows for history navigation
|
|
950
|
-
if (key.upArrow && onNavigateHistory) {
|
|
951
|
-
onNavigateHistory('up');
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
if (key.downArrow && onNavigateHistory) {
|
|
955
|
-
onNavigateHistory('down');
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
// Ignore other control keys, meta, and tab
|
|
959
|
-
if (key.ctrl || key.meta || key.tab) {
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
// Regular character input - insert at cursor position
|
|
963
|
-
if (input) {
|
|
964
|
-
const cursorPos = cursorRef.current;
|
|
965
|
-
const newValue = currentValue.slice(0, cursorPos) + input + currentValue.slice(cursorPos);
|
|
966
|
-
log(`-> char "${input}": "${currentValue}" -> "${newValue}" cursor=${cursorPos}`);
|
|
967
|
-
updateValue(newValue, cursorPos + input.length);
|
|
968
|
-
}
|
|
969
|
-
}, { isActive: !disabled });
|
|
970
|
-
// Determine prompt style based on state
|
|
971
|
-
const promptColor = disabled ? 'gray' : isProcessing ? 'yellow' : 'cyan';
|
|
972
|
-
const isEditing = editingQueueIndex !== null && editingQueueIndex !== undefined;
|
|
973
|
-
const promptText = isProcessing
|
|
974
|
-
? (isEditing ? `edit[${editingQueueIndex + 1}]>` : 'queue>')
|
|
975
|
-
: 'calliope>';
|
|
976
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), suggestions && suggestions.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Tab: " }), _jsx(Text, { color: "cyan", children: suggestions.slice(0, 5).join(' ') }), suggestions.length > 5 && _jsxs(Text, { dimColor: true, children: [" (+", suggestions.length - 5, " more)"] })] })), (queuedCount ?? 0) > 0 && (_jsxs(Box, { children: [_jsxs(Text, { color: "yellow", children: ["\uD83D\uDCE8 ", queuedCount, " queued"] }), _jsx(Text, { dimColor: true, children: " | !msg to send now" })] })), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptText, " "] }), _jsx(Text, { children: value.slice(0, cursorRef.current) }), _jsx(Text, { color: promptColor, children: "\u258C" }), _jsx(Text, { children: value.slice(cursorRef.current) })] })] }));
|
|
977
|
-
}
|
|
978
|
-
// Module-level state for context tracking (persists across renders)
|
|
979
|
-
const contextState = {
|
|
980
|
-
lastLevel: 'healthy',
|
|
981
|
-
warningCounts: { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 },
|
|
982
|
-
lastWarningTime: 0,
|
|
983
|
-
};
|
|
984
|
-
function getContextLevel(percentage) {
|
|
985
|
-
if (percentage >= 98)
|
|
986
|
-
return 'emergency';
|
|
987
|
-
if (percentage >= 95)
|
|
988
|
-
return 'critical';
|
|
989
|
-
if (percentage >= 85)
|
|
990
|
-
return 'warning';
|
|
991
|
-
if (percentage >= 70)
|
|
992
|
-
return 'caution';
|
|
993
|
-
return 'healthy';
|
|
994
|
-
}
|
|
995
|
-
function getContextLevelIndex(level) {
|
|
996
|
-
const order = ['healthy', 'caution', 'warning', 'critical', 'emergency'];
|
|
997
|
-
return order.indexOf(level);
|
|
998
|
-
}
|
|
999
|
-
function shouldShowContextWarning(level) {
|
|
1000
|
-
if (level === 'healthy')
|
|
1001
|
-
return false;
|
|
1002
|
-
const now = Date.now();
|
|
1003
|
-
const timeSinceLastWarning = now - contextState.lastWarningTime;
|
|
1004
|
-
const minInterval = level === 'emergency' ? 30000 : 60000; // 30s for emergency, 60s otherwise
|
|
1005
|
-
// Always warn on level increase
|
|
1006
|
-
if (getContextLevelIndex(level) > getContextLevelIndex(contextState.lastLevel)) {
|
|
1007
|
-
return true;
|
|
1008
|
-
}
|
|
1009
|
-
// Warn again if enough time has passed and we're at critical/emergency
|
|
1010
|
-
if ((level === 'critical' || level === 'emergency') && timeSinceLastWarning > minInterval) {
|
|
1011
|
-
return true;
|
|
1012
|
-
}
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
function checkAndWarnContextLimit(provider, model, tokens, addMessage) {
|
|
1016
|
-
const limit = getModelContextLimit(provider, model);
|
|
1017
|
-
const percentage = (tokens / limit) * 100;
|
|
1018
|
-
const level = getContextLevel(percentage);
|
|
1019
|
-
const used = Math.round(tokens / 1000);
|
|
1020
|
-
const limitK = Math.round(limit / 1000);
|
|
1021
|
-
if (!shouldShowContextWarning(level))
|
|
1022
|
-
return;
|
|
1023
|
-
// Update state
|
|
1024
|
-
contextState.lastLevel = level;
|
|
1025
|
-
contextState.warningCounts[level]++;
|
|
1026
|
-
contextState.lastWarningTime = Date.now();
|
|
1027
|
-
// Generate warning message based on level
|
|
1028
|
-
let message;
|
|
1029
|
-
switch (level) {
|
|
1030
|
-
case 'emergency':
|
|
1031
|
-
message = `\x1b[31m\x1b[1m🚨 EMERGENCY: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
|
|
1032
|
-
\x1b[31m Responses WILL be truncated. Take action NOW:\x1b[0m
|
|
1033
|
-
\x1b[2m /summarize compact - Auto-compress (recommended)
|
|
1034
|
-
/clear - Fresh start
|
|
1035
|
-
/branch new "save" - Save and branch\x1b[0m`;
|
|
1036
|
-
break;
|
|
1037
|
-
case 'critical':
|
|
1038
|
-
message = `\x1b[31m🔴 CRITICAL: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
|
|
1039
|
-
\x1b[2m Approaching limits. Action recommended:
|
|
1040
|
-
/summarize compact | /clear | shorter messages\x1b[0m`;
|
|
1041
|
-
break;
|
|
1042
|
-
case 'warning':
|
|
1043
|
-
message = `\x1b[33m⚠️ WARNING: Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
|
|
1044
|
-
\x1b[2m Consider: /summarize compact | /clear\x1b[0m`;
|
|
1045
|
-
break;
|
|
1046
|
-
case 'caution':
|
|
1047
|
-
message = `\x1b[36m💡 Context at ${Math.round(percentage)}% (${used}K/${limitK}K)\x1b[0m
|
|
1048
|
-
\x1b[2m Monitor usage. /context summary for details\x1b[0m`;
|
|
1049
|
-
break;
|
|
1050
|
-
default:
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
console.log(message + '\n');
|
|
1054
|
-
// Also add to UI messages if callback provided (for critical+)
|
|
1055
|
-
if (addMessage && (level === 'critical' || level === 'emergency')) {
|
|
1056
|
-
const uiMessage = level === 'emergency'
|
|
1057
|
-
? `🚨 EMERGENCY: Context at ${Math.round(percentage)}% - responses will be truncated! Use /summarize compact NOW`
|
|
1058
|
-
: `🔴 Context at ${Math.round(percentage)}% - consider /summarize compact`;
|
|
1059
|
-
addMessage('system', uiMessage);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
function resetContextWarnings() {
|
|
1063
|
-
contextState.lastLevel = 'healthy';
|
|
1064
|
-
contextState.warningCounts = { healthy: 0, caution: 0, warning: 0, critical: 0, emergency: 0 };
|
|
1065
|
-
contextState.lastWarningTime = 0;
|
|
1066
|
-
}
|
|
1067
|
-
function getSmartCommandSuggestions(ctx) {
|
|
1068
|
-
const { input, hasGitRepo, contextPercentage, currentMode, recentCommands } = ctx;
|
|
1069
|
-
if (!input.startsWith('/'))
|
|
1070
|
-
return [];
|
|
1071
|
-
const suggestions = [];
|
|
1072
|
-
const inputLower = input.toLowerCase();
|
|
1073
|
-
// All available commands for matching
|
|
1074
|
-
const allCommands = [
|
|
1075
|
-
'/help', '/clear', '/exit', '/quit',
|
|
1076
|
-
'/mode', '/work', '/plan',
|
|
1077
|
-
'/provider', '/model', '/models', '/config',
|
|
1078
|
-
'/scope', '/add-dir', '/remove-dir', '/find',
|
|
1079
|
-
'/summarize', '/context', '/cost', '/session',
|
|
1080
|
-
'/debug', '/keys', '/unstick', '/flush',
|
|
1081
|
-
'/branch', '/branches', '/switch',
|
|
1082
|
-
'/save', '/load', '/sessions',
|
|
1083
|
-
'/git', '/run', '/set', '/confirm',
|
|
1084
|
-
];
|
|
1085
|
-
// Context-aware prioritization
|
|
1086
|
-
const prioritized = [];
|
|
1087
|
-
// High context? Suggest compaction commands first
|
|
1088
|
-
if (contextPercentage > 70) {
|
|
1089
|
-
prioritized.push('/summarize compact', '/clear', '/branch new');
|
|
1090
|
-
}
|
|
1091
|
-
// Mode-specific suggestions
|
|
1092
|
-
if (currentMode === 'plan') {
|
|
1093
|
-
prioritized.push('/mode hybrid', '/work');
|
|
1094
|
-
}
|
|
1095
|
-
else if (currentMode === 'work') {
|
|
1096
|
-
prioritized.push('/mode hybrid', '/plan');
|
|
1097
|
-
}
|
|
1098
|
-
// Git repo? Suggest git commands
|
|
1099
|
-
if (hasGitRepo) {
|
|
1100
|
-
prioritized.push('/git status', '/git diff', '/git add', '/git commit');
|
|
1101
|
-
}
|
|
1102
|
-
// Add recent commands (deduplicated)
|
|
1103
|
-
for (const cmd of recentCommands.slice(-5)) {
|
|
1104
|
-
if (cmd.startsWith('/') && !prioritized.includes(cmd)) {
|
|
1105
|
-
prioritized.push(cmd);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
// Filter by what user is typing
|
|
1109
|
-
const matchingPrioritized = prioritized.filter(cmd => cmd.toLowerCase().startsWith(inputLower));
|
|
1110
|
-
const matchingAll = allCommands.filter(cmd => cmd.toLowerCase().startsWith(inputLower) && !matchingPrioritized.includes(cmd));
|
|
1111
|
-
suggestions.push(...matchingPrioritized, ...matchingAll);
|
|
1112
|
-
return suggestions.slice(0, 6);
|
|
1113
|
-
}
|
|
1114
|
-
function StatusBar({ provider, model, stats, mode, contextTokens, }) {
|
|
1115
|
-
const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
1116
|
-
const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
|
|
1117
|
-
const displayModel = model.length > 25 ? model.slice(0, 22) + '...' : model;
|
|
1118
|
-
const modeConfig = MODE_CONFIG[mode];
|
|
1119
|
-
// Context usage indicator - uses model's actual context length from API
|
|
1120
|
-
const contextLimit = getModelContextLimit(provider, model);
|
|
1121
|
-
const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100));
|
|
1122
|
-
const contextColor = contextPct > 80 ? 'red' : contextPct > 50 ? 'yellow' : 'green';
|
|
1123
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [modeConfig.icon, " ", modeConfig.label, ' │ ', provider, ":", displayModel, ' │ ', _jsxs(Text, { color: contextColor, children: [formatTokens(contextTokens), "/", formatTokens(contextLimit)] }), ' │ ', formatTokens(stats.inputTokens + stats.outputTokens), " used", ' │ ', formatCost(stats.cost), ' │ ', _jsx(Text, { dimColor: true, children: "Esc: exit" })] })] }));
|
|
1124
|
-
}
|
|
1125
|
-
// ============================================================================
|
|
1126
|
-
// Main Chat Component
|
|
1127
|
-
// ============================================================================
|
|
1128
|
-
function TerminalChat() {
|
|
1129
|
-
const { exit } = useApp();
|
|
1130
|
-
const { stdout } = useStdout();
|
|
1131
|
-
const width = stdout?.columns || 80;
|
|
1132
|
-
// Core state
|
|
1133
|
-
const [input, setInput] = useState('');
|
|
1134
|
-
const [suggestions, setSuggestions] = useState([]);
|
|
1135
|
-
const [messages, setMessages] = useState([]);
|
|
1136
|
-
const [isProcessing, setIsProcessing] = useState(false);
|
|
1137
|
-
const [thinkingState, setThinkingState] = useState(null);
|
|
1138
|
-
const [streamingResponse, setStreamingResponse] = useState('');
|
|
1139
|
-
const [activityState, setActivityState] = useState(null);
|
|
1140
|
-
// Input history for up/down arrow navigation
|
|
1141
|
-
const [inputHistory, setInputHistory] = useState([]);
|
|
1142
|
-
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
1143
|
-
const [savedInput, setSavedInput] = useState(''); // Save current input when navigating
|
|
1144
|
-
// Smart suggestions context
|
|
1145
|
-
const [hasGitRepo] = useState(() => {
|
|
1146
|
-
try {
|
|
1147
|
-
return fs.existsSync('.git') || fs.existsSync('../.git');
|
|
1148
|
-
}
|
|
1149
|
-
catch {
|
|
1150
|
-
return false;
|
|
1151
|
-
}
|
|
1152
|
-
});
|
|
1153
|
-
const recentCommands = React.useMemo(() => inputHistory.filter(cmd => cmd.startsWith('/')).slice(-10), [inputHistory]);
|
|
1154
|
-
// Clear suggestions when input changes significantly
|
|
1155
|
-
const handleInputChange = useCallback((newValue) => {
|
|
1156
|
-
setInput(newValue);
|
|
1157
|
-
// Clear suggestions if user clears input or submits
|
|
1158
|
-
if (!newValue || !newValue.startsWith('/')) {
|
|
1159
|
-
setSuggestions([]);
|
|
1160
|
-
}
|
|
1161
|
-
// Reset history navigation when user types
|
|
1162
|
-
setHistoryIndex(-1);
|
|
1163
|
-
}, []);
|
|
1164
|
-
// Navigate input history
|
|
1165
|
-
const navigateHistory = useCallback((direction) => {
|
|
1166
|
-
if (inputHistory.length === 0)
|
|
1167
|
-
return;
|
|
1168
|
-
if (direction === 'up') {
|
|
1169
|
-
if (historyIndex === -1) {
|
|
1170
|
-
// Save current input before navigating
|
|
1171
|
-
setSavedInput(input);
|
|
1172
|
-
setHistoryIndex(inputHistory.length - 1);
|
|
1173
|
-
setInput(inputHistory[inputHistory.length - 1]);
|
|
1174
|
-
}
|
|
1175
|
-
else if (historyIndex > 0) {
|
|
1176
|
-
setHistoryIndex(historyIndex - 1);
|
|
1177
|
-
setInput(inputHistory[historyIndex - 1]);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
if (historyIndex === -1)
|
|
1182
|
-
return;
|
|
1183
|
-
if (historyIndex < inputHistory.length - 1) {
|
|
1184
|
-
setHistoryIndex(historyIndex + 1);
|
|
1185
|
-
setInput(inputHistory[historyIndex + 1]);
|
|
1186
|
-
}
|
|
1187
|
-
else {
|
|
1188
|
-
// Return to saved input
|
|
1189
|
-
setHistoryIndex(-1);
|
|
1190
|
-
setInput(savedInput);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}, [inputHistory, historyIndex, input, savedInput]);
|
|
1194
|
-
// Add to history when submitting
|
|
1195
|
-
const addToHistory = useCallback((value) => {
|
|
1196
|
-
if (value.trim() && (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== value)) {
|
|
1197
|
-
setInputHistory(prev => [...prev.slice(-100), value]); // Keep last 100 entries
|
|
1198
|
-
}
|
|
1199
|
-
setHistoryIndex(-1);
|
|
1200
|
-
setSavedInput('');
|
|
1201
|
-
}, [inputHistory]);
|
|
1202
|
-
// Config state
|
|
1203
|
-
// Use lazy initializers to avoid calling config.get() on every render
|
|
1204
|
-
const [provider, setProvider] = useState(() => config.get('defaultProvider'));
|
|
1205
|
-
const [model, setModel] = useState(() => config.get('defaultModel'));
|
|
1206
|
-
const [persona, setPersona] = useState(() => config.get('persona'));
|
|
1207
|
-
const [mode, setMode] = useState('hybrid'); // Default to hybrid mode
|
|
1208
|
-
const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
|
|
1209
|
-
const [layout, setLayout] = useState(() => config.get('layout') || 'response-bottom');
|
|
1210
|
-
const [density, setDensity] = useState(() => config.get('density') || 'normal');
|
|
1211
|
-
const [collapseSettings, setCollapseSettings] = useState(() => ({
|
|
1212
|
-
collapseTools: config.get('collapseTools') ?? false,
|
|
1213
|
-
collapseThinking: config.get('collapseThinking') ?? false,
|
|
1214
|
-
toolDisplayLimit: config.get('toolDisplayLimit') ?? 0,
|
|
1215
|
-
}));
|
|
1216
|
-
// Modal state
|
|
1217
|
-
const [modalMode, setModalMode] = useState('none');
|
|
1218
|
-
const [pendingComplexPrompt, setPendingComplexPrompt] = useState(null);
|
|
1219
|
-
const [previousSession, setPreviousSession] = useState(null);
|
|
1220
|
-
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
1221
|
-
const [availableModels, setAvailableModels] = useState([]);
|
|
1222
|
-
const [availableSessions, setAvailableSessions] = useState([]);
|
|
1223
|
-
const [latestVersion, setLatestVersion] = useState(null);
|
|
1224
|
-
// Stats
|
|
1225
|
-
const [stats, setStats] = useState({
|
|
1226
|
-
inputTokens: 0,
|
|
1227
|
-
outputTokens: 0,
|
|
1228
|
-
cost: 0,
|
|
1229
|
-
messageCount: 0,
|
|
1230
|
-
});
|
|
1231
|
-
const [contextTokens, setContextTokens] = useState(0);
|
|
1232
|
-
// Message queue for human-in-the-loop feedback during processing
|
|
1233
|
-
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
1234
|
-
const queuedMessagesRef = useRef([]); // Ref to avoid stale closure in runAgent
|
|
1235
|
-
const [queueInput, setQueueInput] = useState('');
|
|
1236
|
-
const [editingQueueIndex, setEditingQueueIndex] = useState(null);
|
|
1237
|
-
// Keep ref in sync with state
|
|
1238
|
-
useEffect(() => {
|
|
1239
|
-
queuedMessagesRef.current = queuedMessages;
|
|
1240
|
-
}, [queuedMessages]);
|
|
1241
|
-
const undoStack = useRef([]);
|
|
1242
|
-
const redoStack = useRef([]);
|
|
1243
|
-
const MAX_UNDO_HISTORY = 10;
|
|
1244
|
-
const [bookmarks, setBookmarks] = useState([]);
|
|
1245
|
-
const [templates, setTemplates] = useState([]);
|
|
1246
|
-
// Save state before changes (call before modifying messages)
|
|
1247
|
-
const saveUndoState = useCallback(() => {
|
|
1248
|
-
undoStack.current.push({
|
|
1249
|
-
messages: [...messages],
|
|
1250
|
-
llmMessages: [...llmMessages.current],
|
|
1251
|
-
timestamp: new Date(),
|
|
1252
|
-
});
|
|
1253
|
-
// Limit stack size
|
|
1254
|
-
if (undoStack.current.length > MAX_UNDO_HISTORY) {
|
|
1255
|
-
undoStack.current.shift();
|
|
1256
|
-
}
|
|
1257
|
-
// Clear redo stack on new action
|
|
1258
|
-
redoStack.current = [];
|
|
1259
|
-
}, [messages]);
|
|
1260
|
-
// LLM conversation history
|
|
1261
|
-
const llmMessages = useRef([
|
|
1262
|
-
{ role: 'system', content: getSystemPrompt(persona) }
|
|
1263
|
-
]);
|
|
1264
|
-
// Estimate context tokens (rough: ~4 chars per token)
|
|
1265
|
-
const estimateContextTokens = useCallback(() => {
|
|
1266
|
-
let chars = 0;
|
|
1267
|
-
for (const msg of llmMessages.current) {
|
|
1268
|
-
if (typeof msg.content === 'string') {
|
|
1269
|
-
chars += msg.content.length;
|
|
1270
|
-
}
|
|
1271
|
-
else if (Array.isArray(msg.content)) {
|
|
1272
|
-
for (const block of msg.content) {
|
|
1273
|
-
if (block.type === 'text') {
|
|
1274
|
-
chars += block.text.length;
|
|
1275
|
-
}
|
|
1276
|
-
else if (block.type === 'image') {
|
|
1277
|
-
chars += 1000; // Images count as ~250 tokens
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
return Math.round(chars / 4);
|
|
1283
|
-
}, []);
|
|
1284
|
-
// Session state
|
|
1285
|
-
const sessionRef = useRef(null);
|
|
1286
|
-
const [autoRoute, setAutoRoute] = useState(false); // Auto model routing
|
|
1287
|
-
const [memoryLoaded, setMemoryLoaded] = useState(false);
|
|
1288
|
-
// Ralph Wiggum loop state
|
|
1289
|
-
const [loopActive, setLoopActive] = useState(false);
|
|
1290
|
-
const [loopPrompt, setLoopPrompt] = useState('');
|
|
1291
|
-
const [loopMaxIterations, setLoopMaxIterations] = useState(100);
|
|
1292
|
-
const [loopCompletionPromise, setLoopCompletionPromise] = useState();
|
|
1293
|
-
const [loopIteration, setLoopIteration] = useState(0);
|
|
1294
|
-
const loopCancelledRef = useRef(false);
|
|
1295
|
-
// Initialize session and load memory on mount
|
|
1296
|
-
useEffect(() => {
|
|
1297
|
-
const cwd = process.cwd();
|
|
1298
|
-
// Check for existing session with messages
|
|
1299
|
-
const existingSessions = storage.listSessions(5);
|
|
1300
|
-
const recentSession = existingSessions.find(s => s.projectPath === cwd &&
|
|
1301
|
-
s.messageCount > 0 &&
|
|
1302
|
-
Date.now() - new Date(s.lastAccessedAt).getTime() < 24 * 60 * 60 * 1000 // Within 24 hours
|
|
1303
|
-
);
|
|
1304
|
-
if (recentSession && !sessionRef.current) {
|
|
1305
|
-
// Offer to resume
|
|
1306
|
-
setPreviousSession({
|
|
1307
|
-
projectName: recentSession.projectName,
|
|
1308
|
-
lastAccessedAt: recentSession.lastAccessedAt,
|
|
1309
|
-
messageCount: recentSession.messageCount,
|
|
1310
|
-
});
|
|
1311
|
-
setModalMode('session-resume');
|
|
1312
|
-
}
|
|
1313
|
-
const session = storage.getOrCreateSession(cwd);
|
|
1314
|
-
sessionRef.current = session;
|
|
1315
|
-
// Load memory context into system prompt
|
|
1316
|
-
if (!memoryLoaded) {
|
|
1317
|
-
const cwd = process.cwd();
|
|
1318
|
-
const memoryContext = memory.buildMemoryContext(cwd);
|
|
1319
|
-
if (memoryContext.trim()) {
|
|
1320
|
-
// Append memory context to system prompt
|
|
1321
|
-
const currentSystem = llmMessages.current[0];
|
|
1322
|
-
if (currentSystem && currentSystem.role === 'system') {
|
|
1323
|
-
const systemContent = typeof currentSystem.content === 'string'
|
|
1324
|
-
? currentSystem.content
|
|
1325
|
-
: '';
|
|
1326
|
-
llmMessages.current[0] = {
|
|
1327
|
-
role: 'system',
|
|
1328
|
-
content: systemContent + '\n\n--- Project Context ---\n' + memoryContext,
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
setMemoryLoaded(true);
|
|
1333
|
-
// Execute session start hooks
|
|
1334
|
-
hooks.executeHooks('session-start', {}).catch((err) => {
|
|
1335
|
-
debugLog('hooks', 'session-start hook failed:', err instanceof Error ? err.message : err);
|
|
1336
|
-
});
|
|
1337
|
-
// Load templates from storage
|
|
1338
|
-
const savedTemplates = storage.getTemplates();
|
|
1339
|
-
if (savedTemplates.length > 0) {
|
|
1340
|
-
setTemplates(savedTemplates.map(t => ({
|
|
1341
|
-
name: t.name,
|
|
1342
|
-
prompt: t.prompt,
|
|
1343
|
-
createdAt: new Date(t.createdAt),
|
|
1344
|
-
})));
|
|
1345
|
-
}
|
|
1346
|
-
// Pre-warm model cache in background for faster model switching
|
|
1347
|
-
preWarmModelCache().catch((err) => {
|
|
1348
|
-
debugLog('cache', 'model cache pre-warm failed:', err instanceof Error ? err.message : err);
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
}, [memoryLoaded]);
|
|
1352
|
-
// Derived values
|
|
1353
|
-
const actualProvider = selectProvider(provider);
|
|
1354
|
-
const actualModel = model || DEFAULT_MODELS[actualProvider];
|
|
1355
|
-
const isModalActive = modalMode !== 'none';
|
|
1356
|
-
// Add message helper
|
|
1357
|
-
const addMessage = useCallback((type, content) => {
|
|
1358
|
-
setMessages(prev => [...prev, {
|
|
1359
|
-
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1360
|
-
type,
|
|
1361
|
-
content
|
|
1362
|
-
}]);
|
|
1363
|
-
// Persist user and assistant messages to storage for session history
|
|
1364
|
-
if (type === 'user' || type === 'assistant') {
|
|
1365
|
-
storage.addChatMessage({ role: type, content });
|
|
1366
|
-
}
|
|
1367
|
-
}, []);
|
|
1368
|
-
// Handler to edit or delete a queued message
|
|
1369
|
-
const handleEditQueuedMessage = useCallback((index, newMsg) => {
|
|
1370
|
-
if (newMsg === '') {
|
|
1371
|
-
// Delete the message
|
|
1372
|
-
setQueuedMessages(prev => prev.filter((_, i) => i !== index));
|
|
1373
|
-
addMessage('system', `🗑️ Deleted queued message #${index + 1}`);
|
|
1374
|
-
}
|
|
1375
|
-
else {
|
|
1376
|
-
// Update the message
|
|
1377
|
-
setQueuedMessages(prev => prev.map((msg, i) => i === index ? newMsg : msg));
|
|
1378
|
-
addMessage('system', `✏️ Updated queued message #${index + 1}`);
|
|
1379
|
-
}
|
|
1380
|
-
}, [addMessage]);
|
|
1381
|
-
// Handle slash commands
|
|
1382
|
-
const handleCommand = useCallback(async (cmd) => {
|
|
1383
|
-
const parts = cmd.split(/\s+/);
|
|
1384
|
-
const command = parts[0].toLowerCase();
|
|
1385
|
-
switch (command) {
|
|
1386
|
-
case '/help':
|
|
1387
|
-
case '/h':
|
|
1388
|
-
addMessage('system', `Commands:
|
|
1389
|
-
/mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
|
|
1390
|
-
/provider [name] - Switch AI provider
|
|
1391
|
-
/model [name] - Switch model
|
|
1392
|
-
/route [on|off|test] - Auto model routing by complexity
|
|
1393
|
-
/persona [name] - Switch personality
|
|
1394
|
-
/todo [add|done|list] - Manage TODOs
|
|
1395
|
-
/plans [list|view] - View plan history
|
|
1396
|
-
/session [list|info] - Session management
|
|
1397
|
-
/history [search] - Chat history
|
|
1398
|
-
/context [load|summary] - Context management
|
|
1399
|
-
/summarize [context|compact] - Summarize/compact context
|
|
1400
|
-
/clear - Clear conversation
|
|
1401
|
-
/copy - Copy last response to clipboard
|
|
1402
|
-
/export [file.md] - Export conversation to markdown
|
|
1403
|
-
/edit - Edit and resend last message
|
|
1404
|
-
/undo - Undo last action (up to 10 steps)
|
|
1405
|
-
/redo - Redo undone action
|
|
1406
|
-
/confirm [on|off] - Toggle risky op confirmation
|
|
1407
|
-
/profile [name|save|del] - Switch/save/delete profiles
|
|
1408
|
-
/mcp [add|remove|tools] - Manage MCP servers
|
|
1409
|
-
/skills [add|remove] - Manage agent skills
|
|
1410
|
-
/memory [init|add|show] - Project memory (CALLIOPE.md)
|
|
1411
|
-
/project [init|show|run] - Project config (.calliope)
|
|
1412
|
-
/find <pattern> - Fuzzy file search
|
|
1413
|
-
/branch [new|switch] - Conversation branches
|
|
1414
|
-
/theme [name|list] - Color themes
|
|
1415
|
-
/hooks [list|add] - Pre/post tool hooks
|
|
1416
|
-
/search <query> - Search conversation
|
|
1417
|
-
/status - Show status
|
|
1418
|
-
/config - Show config
|
|
1419
|
-
/layout [name] - Switch UI layout (classic/split/etc)
|
|
1420
|
-
/density [normal|compact] - Set display density
|
|
1421
|
-
/collapse [tools|all|off] - Collapse/expand tool output
|
|
1422
|
-
/upgrade - Check for updates
|
|
1423
|
-
/agents - Show sub-agent status (--agterm mode)
|
|
1424
|
-
/scope [details|reset] - Show/manage file access scope
|
|
1425
|
-
/add-dir <path> - Add directory to allowed scope
|
|
1426
|
-
/remove-dir <path> - Remove directory from scope
|
|
1427
|
-
/template [save|use|del] - Manage prompt templates
|
|
1428
|
-
/cost - Show cost tracking summary
|
|
1429
|
-
/bookmark [name] - Create bookmark at current point
|
|
1430
|
-
/bookmark list - List all bookmarks
|
|
1431
|
-
/bookmark goto <n> - Jump to bookmark
|
|
1432
|
-
/queue [show|clear|flush] - Manage queued messages
|
|
1433
|
-
/flush - Force-process queued msgs (unstick)
|
|
1434
|
-
/debug [on|off] - Show state / toggle debug logging
|
|
1435
|
-
/unstick - Emergency reset of processing state
|
|
1436
|
-
/work - Quick switch to work mode
|
|
1437
|
-
/plan - Quick switch to plan mode
|
|
1438
|
-
/keys or /? - Show keyboard shortcuts
|
|
1439
|
-
/resume [n] - Resume previous session (load n messages)
|
|
1440
|
-
/exit - Exit
|
|
1441
|
-
|
|
1442
|
-
File references: @filename, ./path, /absolute/path
|
|
1443
|
-
Modes: 📋 Plan | 🔄 Hybrid | 🔧 Work
|
|
1444
|
-
Queue: ↑/↓ edit, Ctrl+D delete, !msg to send directly
|
|
1445
|
-
Auto-route: ${autoRoute ? 'ON' : 'OFF'}${moduleAgtermEnabled ? '\nAGTerm: ON (spawn_agent, check_agent tools available)' : ''}`);
|
|
1446
|
-
break;
|
|
1447
|
-
case '/provider':
|
|
1448
|
-
case '/p':
|
|
1449
|
-
if (parts[1]) {
|
|
1450
|
-
const p = parts[1].toLowerCase();
|
|
1451
|
-
setProvider(p);
|
|
1452
|
-
addMessage('system', `Provider: ${selectProvider(p)}`);
|
|
1453
|
-
}
|
|
1454
|
-
else {
|
|
1455
|
-
addMessage('system', `Provider: ${actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
|
|
1456
|
-
}
|
|
1457
|
-
break;
|
|
1458
|
-
case '/model':
|
|
1459
|
-
case '/m':
|
|
1460
|
-
if (parts[1]) {
|
|
1461
|
-
setModel(parts[1]);
|
|
1462
|
-
addMessage('system', `Model: ${parts[1]}`);
|
|
1463
|
-
}
|
|
1464
|
-
else {
|
|
1465
|
-
addMessage('system', `Discovering models for ${actualProvider}...`);
|
|
1466
|
-
try {
|
|
1467
|
-
const models = await getAvailableModels(actualProvider);
|
|
1468
|
-
if (models.length > 0) {
|
|
1469
|
-
setAvailableModels(models);
|
|
1470
|
-
setModalMode('model');
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
addMessage('error', 'No models found');
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
catch (e) {
|
|
1477
|
-
addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
break;
|
|
1481
|
-
case '/models':
|
|
1482
|
-
addMessage('system', `Discovering models for ${actualProvider}...`);
|
|
1483
|
-
try {
|
|
1484
|
-
const models = await getAvailableModels(actualProvider);
|
|
1485
|
-
if (models.length > 0) {
|
|
1486
|
-
setAvailableModels(models);
|
|
1487
|
-
setModalMode('model');
|
|
1488
|
-
}
|
|
1489
|
-
else {
|
|
1490
|
-
addMessage('error', 'No models found');
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
catch (e) {
|
|
1494
|
-
addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
|
|
1495
|
-
}
|
|
1496
|
-
break;
|
|
1497
|
-
case '/mode':
|
|
1498
|
-
if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
|
|
1499
|
-
const m = parts[1];
|
|
1500
|
-
setMode(m);
|
|
1501
|
-
addMessage('system', `Mode: ${MODE_CONFIG[m].icon} ${MODE_CONFIG[m].label} - ${MODE_CONFIG[m].description}`);
|
|
1502
|
-
}
|
|
1503
|
-
else {
|
|
1504
|
-
const currentConfig = MODE_CONFIG[mode];
|
|
1505
|
-
addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (📋), hybrid (🔄), work (🔧)\nUse Shift+Tab to cycle`);
|
|
1506
|
-
}
|
|
1507
|
-
break;
|
|
1508
|
-
case '/persona':
|
|
1509
|
-
if (parts[1] && ['calliope', 'professional', 'minimal'].includes(parts[1])) {
|
|
1510
|
-
const p = parts[1];
|
|
1511
|
-
setPersona(p);
|
|
1512
|
-
llmMessages.current = [{ role: 'system', content: getSystemPrompt(p) }];
|
|
1513
|
-
addMessage('system', `Persona: ${p}`);
|
|
1514
|
-
}
|
|
1515
|
-
else {
|
|
1516
|
-
addMessage('system', `Persona: ${persona} | Options: calliope, professional, minimal`);
|
|
1517
|
-
}
|
|
1518
|
-
break;
|
|
1519
|
-
case '/clear':
|
|
1520
|
-
case '/c':
|
|
1521
|
-
setMessages([]);
|
|
1522
|
-
llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
|
|
1523
|
-
setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
|
|
1524
|
-
resetContextWarnings(); // Reset context warning state
|
|
1525
|
-
break;
|
|
1526
|
-
case '/copy': {
|
|
1527
|
-
// Copy last assistant response to clipboard
|
|
1528
|
-
const lastAssistant = [...messages].reverse().find(m => m.type === 'assistant');
|
|
1529
|
-
if (lastAssistant) {
|
|
1530
|
-
try {
|
|
1531
|
-
const { execSync } = await import('child_process');
|
|
1532
|
-
// Try different clipboard commands based on platform
|
|
1533
|
-
const content = lastAssistant.content;
|
|
1534
|
-
if (process.platform === 'darwin') {
|
|
1535
|
-
execSync('pbcopy', { input: content });
|
|
1536
|
-
}
|
|
1537
|
-
else if (process.platform === 'win32') {
|
|
1538
|
-
execSync('clip', { input: content });
|
|
1539
|
-
}
|
|
1540
|
-
else {
|
|
1541
|
-
// Linux - try xclip, xsel, or wl-copy
|
|
1542
|
-
try {
|
|
1543
|
-
execSync('xclip -selection clipboard', { input: content });
|
|
1544
|
-
}
|
|
1545
|
-
catch {
|
|
1546
|
-
try {
|
|
1547
|
-
execSync('xsel --clipboard --input', { input: content });
|
|
1548
|
-
}
|
|
1549
|
-
catch {
|
|
1550
|
-
execSync('wl-copy', { input: content });
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
addMessage('system', '✓ Copied to clipboard');
|
|
1555
|
-
}
|
|
1556
|
-
catch (e) {
|
|
1557
|
-
addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
else {
|
|
1561
|
-
addMessage('system', 'No assistant message to copy');
|
|
1562
|
-
}
|
|
1563
|
-
break;
|
|
1564
|
-
}
|
|
1565
|
-
case '/export': {
|
|
1566
|
-
// Export conversation to markdown
|
|
1567
|
-
const filename = parts[1] || `calliope-export-${Date.now()}.md`;
|
|
1568
|
-
const fs = await import('fs');
|
|
1569
|
-
const path = await import('path');
|
|
1570
|
-
let markdown = `# Calliope Conversation Export\n\n`;
|
|
1571
|
-
markdown += `**Date:** ${new Date().toLocaleString()}\n`;
|
|
1572
|
-
markdown += `**Provider:** ${actualProvider}\n`;
|
|
1573
|
-
markdown += `**Model:** ${actualModel}\n\n---\n\n`;
|
|
1574
|
-
for (const msg of messages) {
|
|
1575
|
-
if (msg.type === 'user') {
|
|
1576
|
-
markdown += `## 👤 User\n\n${msg.content}\n\n`;
|
|
1577
|
-
}
|
|
1578
|
-
else if (msg.type === 'assistant') {
|
|
1579
|
-
markdown += `## 🤖 Assistant\n\n${msg.content}\n\n`;
|
|
1580
|
-
}
|
|
1581
|
-
else if (msg.type === 'tool') {
|
|
1582
|
-
markdown += `> 🔧 Tool: ${msg.content}\n\n`;
|
|
1583
|
-
}
|
|
1584
|
-
else if (msg.type === 'system') {
|
|
1585
|
-
markdown += `> ℹ️ ${msg.content}\n\n`;
|
|
1586
|
-
}
|
|
1587
|
-
else if (msg.type === 'error') {
|
|
1588
|
-
markdown += `> ⚠️ Error: ${msg.content}\n\n`;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
const filepath = path.resolve(process.cwd(), filename);
|
|
1592
|
-
fs.writeFileSync(filepath, markdown);
|
|
1593
|
-
addMessage('system', `✓ Exported to ${filename}`);
|
|
1594
|
-
break;
|
|
1595
|
-
}
|
|
1596
|
-
case '/edit': {
|
|
1597
|
-
// Edit last user message
|
|
1598
|
-
const lastUserIdx = [...messages].reverse().findIndex(m => m.type === 'user');
|
|
1599
|
-
if (lastUserIdx >= 0) {
|
|
1600
|
-
const lastUser = messages[messages.length - 1 - lastUserIdx];
|
|
1601
|
-
setInput(lastUser.content);
|
|
1602
|
-
addMessage('system', 'Edit the message above and press Enter to resend');
|
|
1603
|
-
}
|
|
1604
|
-
else {
|
|
1605
|
-
addMessage('system', 'No user message to edit');
|
|
1606
|
-
}
|
|
1607
|
-
break;
|
|
1608
|
-
}
|
|
1609
|
-
case '/undo': {
|
|
1610
|
-
if (undoStack.current.length === 0) {
|
|
1611
|
-
addMessage('system', 'Nothing to undo.');
|
|
1612
|
-
break;
|
|
1613
|
-
}
|
|
1614
|
-
// Save current state to redo stack
|
|
1615
|
-
redoStack.current.push({
|
|
1616
|
-
messages: [...messages],
|
|
1617
|
-
llmMessages: [...llmMessages.current],
|
|
1618
|
-
timestamp: new Date(),
|
|
1619
|
-
});
|
|
1620
|
-
// Restore previous state
|
|
1621
|
-
const prevState = undoStack.current.pop();
|
|
1622
|
-
setMessages(prevState.messages);
|
|
1623
|
-
llmMessages.current = prevState.llmMessages;
|
|
1624
|
-
setContextTokens(estimateContextTokens());
|
|
1625
|
-
addMessage('system', `✓ Undone (${undoStack.current.length} more available)`);
|
|
1626
|
-
break;
|
|
1627
|
-
}
|
|
1628
|
-
case '/redo': {
|
|
1629
|
-
if (redoStack.current.length === 0) {
|
|
1630
|
-
addMessage('system', 'Nothing to redo.');
|
|
1631
|
-
break;
|
|
1632
|
-
}
|
|
1633
|
-
// Save current state to undo stack
|
|
1634
|
-
undoStack.current.push({
|
|
1635
|
-
messages: [...messages],
|
|
1636
|
-
llmMessages: [...llmMessages.current],
|
|
1637
|
-
timestamp: new Date(),
|
|
1638
|
-
});
|
|
1639
|
-
// Restore redo state
|
|
1640
|
-
const redoState = redoStack.current.pop();
|
|
1641
|
-
setMessages(redoState.messages);
|
|
1642
|
-
llmMessages.current = redoState.llmMessages;
|
|
1643
|
-
setContextTokens(estimateContextTokens());
|
|
1644
|
-
addMessage('system', `✓ Redone (${redoStack.current.length} more available)`);
|
|
1645
|
-
break;
|
|
1646
|
-
}
|
|
1647
|
-
case '/status':
|
|
1648
|
-
case '/s':
|
|
1649
|
-
addMessage('system', `${actualProvider}:${actualModel} | ${stats.messageCount} msgs | ${stats.inputTokens + stats.outputTokens} tokens`);
|
|
1650
|
-
break;
|
|
1651
|
-
case '/config':
|
|
1652
|
-
addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}`);
|
|
1653
|
-
break;
|
|
1654
|
-
case '/agents':
|
|
1655
|
-
if (!moduleAgtermEnabled) {
|
|
1656
|
-
addMessage('system', 'AGTerm mode not enabled. Start with --agterm flag to unlock multi-agent features.');
|
|
1657
|
-
}
|
|
1658
|
-
else {
|
|
1659
|
-
addMessage('system', getAgentStatusReport());
|
|
1660
|
-
}
|
|
1661
|
-
break;
|
|
1662
|
-
case '/set': {
|
|
1663
|
-
// /set <key> <value>
|
|
1664
|
-
const key = parts[1];
|
|
1665
|
-
const value = parts.slice(2).join(' ');
|
|
1666
|
-
if (!key || !value) {
|
|
1667
|
-
addMessage('system', `Usage: /set <key> <value>
|
|
1668
|
-
Available keys:
|
|
1669
|
-
maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
|
|
1670
|
-
persona <name> - calliope, professional, minimal
|
|
1671
|
-
fancyOutput <bool> - true/false`);
|
|
1672
|
-
break;
|
|
1673
|
-
}
|
|
1674
|
-
try {
|
|
1675
|
-
if (key === 'maxIterations') {
|
|
1676
|
-
const num = parseInt(value, 10);
|
|
1677
|
-
if (isNaN(num) || num < 1 || num > 10000) {
|
|
1678
|
-
addMessage('error', 'maxIterations must be 1-10000');
|
|
1679
|
-
break;
|
|
1680
|
-
}
|
|
1681
|
-
config.set('maxIterations', num);
|
|
1682
|
-
addMessage('system', `✓ maxIterations set to ${num}`);
|
|
1683
|
-
}
|
|
1684
|
-
else if (key === 'persona') {
|
|
1685
|
-
if (!['calliope', 'professional', 'minimal'].includes(value)) {
|
|
1686
|
-
addMessage('error', 'persona must be: calliope, professional, or minimal');
|
|
1687
|
-
break;
|
|
1688
|
-
}
|
|
1689
|
-
config.set('persona', value);
|
|
1690
|
-
setPersona(value);
|
|
1691
|
-
addMessage('system', `✓ persona set to ${value}`);
|
|
1692
|
-
}
|
|
1693
|
-
else if (key === 'fancyOutput') {
|
|
1694
|
-
const bool = value === 'true';
|
|
1695
|
-
config.set('fancyOutput', bool);
|
|
1696
|
-
addMessage('system', `✓ fancyOutput set to ${bool}`);
|
|
1697
|
-
}
|
|
1698
|
-
else {
|
|
1699
|
-
addMessage('error', `Unknown config key: ${key}`);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
catch (err) {
|
|
1703
|
-
addMessage('error', `Failed to set ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1704
|
-
}
|
|
1705
|
-
break;
|
|
1706
|
-
}
|
|
1707
|
-
case '/setup':
|
|
1708
|
-
addMessage('system', 'Run `calliope --setup` to reconfigure.');
|
|
1709
|
-
break;
|
|
1710
|
-
case '/layout': {
|
|
1711
|
-
// /layout [classic|response-top|response-bottom|split]
|
|
1712
|
-
const layoutArg = parts[1];
|
|
1713
|
-
if (!layoutArg) {
|
|
1714
|
-
addMessage('system', `Current layout: ${layout}
|
|
1715
|
-
|
|
1716
|
-
Available layouts:
|
|
1717
|
-
classic - Everything in chronological order
|
|
1718
|
-
response-top - Calliope response at top, tools below
|
|
1719
|
-
response-bottom - Tools at top, response at bottom (default)
|
|
1720
|
-
split - Side by side: tools left, response right
|
|
1721
|
-
|
|
1722
|
-
Usage: /layout <name>`);
|
|
1723
|
-
break;
|
|
1724
|
-
}
|
|
1725
|
-
const validLayouts = ['classic', 'response-top', 'response-bottom', 'split'];
|
|
1726
|
-
if (!validLayouts.includes(layoutArg)) {
|
|
1727
|
-
addMessage('error', `Invalid layout. Choose: ${validLayouts.join(', ')}`);
|
|
1728
|
-
break;
|
|
1729
|
-
}
|
|
1730
|
-
config.set('layout', layoutArg);
|
|
1731
|
-
setLayout(layoutArg);
|
|
1732
|
-
addMessage('system', `✓ Layout set to: ${layoutArg}`);
|
|
1733
|
-
break;
|
|
1734
|
-
}
|
|
1735
|
-
case '/density': {
|
|
1736
|
-
// /density [normal|compact]
|
|
1737
|
-
const densityArg = parts[1];
|
|
1738
|
-
if (!densityArg) {
|
|
1739
|
-
addMessage('system', `Current density: ${density}
|
|
1740
|
-
|
|
1741
|
-
Available densities:
|
|
1742
|
-
normal - Standard spacing
|
|
1743
|
-
compact - Reduced whitespace for more info
|
|
1744
|
-
|
|
1745
|
-
Usage: /density <normal|compact>`);
|
|
1746
|
-
break;
|
|
1747
|
-
}
|
|
1748
|
-
const validDensities = ['normal', 'compact'];
|
|
1749
|
-
if (!validDensities.includes(densityArg)) {
|
|
1750
|
-
addMessage('error', `Invalid density. Choose: normal, compact`);
|
|
1751
|
-
break;
|
|
1752
|
-
}
|
|
1753
|
-
config.set('density', densityArg);
|
|
1754
|
-
setDensity(densityArg);
|
|
1755
|
-
addMessage('system', `✓ Density set to: ${densityArg}`);
|
|
1756
|
-
break;
|
|
1757
|
-
}
|
|
1758
|
-
case '/collapse': {
|
|
1759
|
-
// /collapse [tools|thinking|all|off] [limit N]
|
|
1760
|
-
const subCmd = parts[1];
|
|
1761
|
-
if (!subCmd) {
|
|
1762
|
-
addMessage('system', `Collapse settings:
|
|
1763
|
-
collapseTools: ${collapseSettings.collapseTools}
|
|
1764
|
-
collapseThinking: ${collapseSettings.collapseThinking}
|
|
1765
|
-
toolDisplayLimit: ${collapseSettings.toolDisplayLimit} (0 = all expanded)
|
|
1766
|
-
|
|
1767
|
-
Usage:
|
|
1768
|
-
/collapse tools - Toggle tool output collapsing
|
|
1769
|
-
/collapse thinking - Toggle thinking block collapsing
|
|
1770
|
-
/collapse all - Collapse both tools and thinking
|
|
1771
|
-
/collapse off - Expand everything
|
|
1772
|
-
/collapse limit <N> - Show last N tools expanded (0 = all)`);
|
|
1773
|
-
break;
|
|
1774
|
-
}
|
|
1775
|
-
if (subCmd === 'tools') {
|
|
1776
|
-
const newVal = !collapseSettings.collapseTools;
|
|
1777
|
-
config.set('collapseTools', newVal);
|
|
1778
|
-
setCollapseSettings(prev => ({ ...prev, collapseTools: newVal }));
|
|
1779
|
-
addMessage('system', `✓ collapseTools set to ${newVal}`);
|
|
1780
|
-
}
|
|
1781
|
-
else if (subCmd === 'thinking') {
|
|
1782
|
-
const newVal = !collapseSettings.collapseThinking;
|
|
1783
|
-
config.set('collapseThinking', newVal);
|
|
1784
|
-
setCollapseSettings(prev => ({ ...prev, collapseThinking: newVal }));
|
|
1785
|
-
addMessage('system', `✓ collapseThinking set to ${newVal}`);
|
|
1786
|
-
}
|
|
1787
|
-
else if (subCmd === 'all') {
|
|
1788
|
-
config.set('collapseTools', true);
|
|
1789
|
-
config.set('collapseThinking', true);
|
|
1790
|
-
setCollapseSettings(prev => ({ ...prev, collapseTools: true, collapseThinking: true }));
|
|
1791
|
-
addMessage('system', '✓ Collapsing tools and thinking');
|
|
1792
|
-
}
|
|
1793
|
-
else if (subCmd === 'off') {
|
|
1794
|
-
config.set('collapseTools', false);
|
|
1795
|
-
config.set('collapseThinking', false);
|
|
1796
|
-
setCollapseSettings(prev => ({ ...prev, collapseTools: false, collapseThinking: false }));
|
|
1797
|
-
addMessage('system', '✓ Expanding all output');
|
|
1798
|
-
}
|
|
1799
|
-
else if (subCmd === 'limit') {
|
|
1800
|
-
const limit = parseInt(parts[2], 10);
|
|
1801
|
-
if (isNaN(limit) || limit < 0 || limit > 100) {
|
|
1802
|
-
addMessage('error', 'Limit must be 0-100');
|
|
1803
|
-
break;
|
|
1804
|
-
}
|
|
1805
|
-
config.set('toolDisplayLimit', limit);
|
|
1806
|
-
setCollapseSettings(prev => ({ ...prev, toolDisplayLimit: limit }));
|
|
1807
|
-
addMessage('system', `✓ toolDisplayLimit set to ${limit}`);
|
|
1808
|
-
}
|
|
1809
|
-
else {
|
|
1810
|
-
addMessage('error', 'Unknown collapse option. Use: tools, thinking, all, off, or limit <N>');
|
|
1811
|
-
}
|
|
1812
|
-
break;
|
|
1813
|
-
}
|
|
1814
|
-
case '/loop': {
|
|
1815
|
-
// Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
|
|
1816
|
-
const loopArgs = parts.slice(1).join(' ');
|
|
1817
|
-
const maxIterMatch = loopArgs.match(/--max-iterations\s+(\d+)/);
|
|
1818
|
-
const completionMatch = loopArgs.match(/--completion-promise\s+"([^"]+)"/);
|
|
1819
|
-
let prompt = loopArgs
|
|
1820
|
-
.replace(/--max-iterations\s+\d+/, '')
|
|
1821
|
-
.replace(/--completion-promise\s+"[^"]+"/, '')
|
|
1822
|
-
.trim();
|
|
1823
|
-
// Handle quoted prompt
|
|
1824
|
-
const quotedMatch = prompt.match(/^"([^"]+)"$/);
|
|
1825
|
-
if (quotedMatch)
|
|
1826
|
-
prompt = quotedMatch[1];
|
|
1827
|
-
if (!prompt) {
|
|
1828
|
-
addMessage('system', `Usage: /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
|
|
1829
|
-
Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"`);
|
|
1830
|
-
break;
|
|
1831
|
-
}
|
|
1832
|
-
// Start the loop
|
|
1833
|
-
setLoopActive(true);
|
|
1834
|
-
setLoopPrompt(prompt);
|
|
1835
|
-
setLoopMaxIterations(maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100);
|
|
1836
|
-
setLoopCompletionPromise(completionMatch ? completionMatch[1] : undefined);
|
|
1837
|
-
setLoopIteration(0);
|
|
1838
|
-
loopCancelledRef.current = false;
|
|
1839
|
-
addMessage('system', `🔄 Ralph Wiggum Loop Started
|
|
1840
|
-
Prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
|
|
1841
|
-
Max iterations: ${maxIterMatch ? maxIterMatch[1] : '100'}
|
|
1842
|
-
${completionMatch ? `Completion promise: "${completionMatch[1]}"` : 'No completion promise (runs until max iterations)'}
|
|
1843
|
-
Use /cancel-loop to stop`);
|
|
1844
|
-
// Start the loop execution (non-blocking)
|
|
1845
|
-
runLoop(prompt, maxIterMatch ? parseInt(maxIterMatch[1], 10) : 100, completionMatch?.[1]);
|
|
1846
|
-
break;
|
|
1847
|
-
}
|
|
1848
|
-
case '/cancel-loop':
|
|
1849
|
-
case '/stop':
|
|
1850
|
-
if (loopActive) {
|
|
1851
|
-
loopCancelledRef.current = true;
|
|
1852
|
-
setLoopActive(false);
|
|
1853
|
-
addMessage('system', '🛑 Loop cancelled');
|
|
1854
|
-
}
|
|
1855
|
-
else {
|
|
1856
|
-
addMessage('system', 'No active loop to cancel');
|
|
1857
|
-
}
|
|
1858
|
-
break;
|
|
1859
|
-
case '/confirm':
|
|
1860
|
-
if (parts[1] === 'on') {
|
|
1861
|
-
setConfirmMode(true);
|
|
1862
|
-
addMessage('system', '✓ Confirmation mode ON - will ask before risky operations');
|
|
1863
|
-
}
|
|
1864
|
-
else if (parts[1] === 'off') {
|
|
1865
|
-
setConfirmMode(false);
|
|
1866
|
-
addMessage('system', '⚠️ Confirmation mode OFF - risky operations will auto-execute');
|
|
1867
|
-
}
|
|
1868
|
-
else {
|
|
1869
|
-
addMessage('system', `Confirm mode: ${confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
|
|
1870
|
-
}
|
|
1871
|
-
break;
|
|
1872
|
-
case '/profile': {
|
|
1873
|
-
const subCmd = parts[1];
|
|
1874
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1875
|
-
const profiles = config.listProfiles();
|
|
1876
|
-
const active = config.getActiveProfile();
|
|
1877
|
-
const list = profiles.map(p => {
|
|
1878
|
-
const marker = p.name === active ? '→ ' : ' ';
|
|
1879
|
-
const tag = p.builtin ? '(built-in)' : '(custom)';
|
|
1880
|
-
return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
|
|
1881
|
-
}).join('\n');
|
|
1882
|
-
addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
|
|
1883
|
-
}
|
|
1884
|
-
else if (subCmd === 'save' && parts[2]) {
|
|
1885
|
-
const name = parts[2];
|
|
1886
|
-
config.saveProfile(name, {
|
|
1887
|
-
provider: provider,
|
|
1888
|
-
model: model,
|
|
1889
|
-
persona: persona,
|
|
1890
|
-
confirmMode: confirmMode,
|
|
1891
|
-
});
|
|
1892
|
-
addMessage('system', `✓ Saved profile: ${name}`);
|
|
1893
|
-
}
|
|
1894
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
1895
|
-
const name = parts[2];
|
|
1896
|
-
if (config.deleteProfile(name)) {
|
|
1897
|
-
addMessage('system', `✓ Deleted profile: ${name}`);
|
|
1898
|
-
}
|
|
1899
|
-
else {
|
|
1900
|
-
addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
else {
|
|
1904
|
-
// Load profile
|
|
1905
|
-
const profile = config.getProfile(subCmd);
|
|
1906
|
-
if (profile) {
|
|
1907
|
-
setProvider(profile.provider);
|
|
1908
|
-
if (profile.model)
|
|
1909
|
-
setModel(profile.model);
|
|
1910
|
-
setPersona(profile.persona);
|
|
1911
|
-
if (profile.confirmMode !== undefined)
|
|
1912
|
-
setConfirmMode(profile.confirmMode);
|
|
1913
|
-
config.setActiveProfile(subCmd);
|
|
1914
|
-
addMessage('system', `✓ Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
|
|
1915
|
-
}
|
|
1916
|
-
else {
|
|
1917
|
-
addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
break;
|
|
1921
|
-
}
|
|
1922
|
-
case '/mcp': {
|
|
1923
|
-
const subCmd = parts[1];
|
|
1924
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1925
|
-
const servers = mcp.listServers();
|
|
1926
|
-
if (servers.length === 0) {
|
|
1927
|
-
addMessage('system', 'No MCP servers registered.\n\nUsage:\n /mcp add <url> - Register MCP server\n /mcp remove <id> - Remove server');
|
|
1928
|
-
}
|
|
1929
|
-
else {
|
|
1930
|
-
const list = servers.map(s => {
|
|
1931
|
-
const status = s.status === 'connected' ? '🟢' : s.status === 'error' ? '🔴' : '⚪';
|
|
1932
|
-
return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
|
|
1933
|
-
}).join('\n\n');
|
|
1934
|
-
addMessage('system', `MCP Servers:\n\n${list}`);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
else if (subCmd === 'add' && parts[2]) {
|
|
1938
|
-
const url = parts[2];
|
|
1939
|
-
addMessage('system', `Registering MCP server: ${url}...`);
|
|
1940
|
-
try {
|
|
1941
|
-
const server = await mcp.registerServer(url);
|
|
1942
|
-
addMessage('system', `✓ Registered: ${server.name} (${server.tools.length} tools)`);
|
|
1943
|
-
}
|
|
1944
|
-
catch (e) {
|
|
1945
|
-
addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
1949
|
-
if (mcp.unregisterServer(parts[2])) {
|
|
1950
|
-
addMessage('system', '✓ Server removed');
|
|
1951
|
-
}
|
|
1952
|
-
else {
|
|
1953
|
-
addMessage('error', 'Server not found');
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
else if (subCmd === 'refresh') {
|
|
1957
|
-
const servers = mcp.listServers();
|
|
1958
|
-
let connected = 0;
|
|
1959
|
-
for (const s of servers) {
|
|
1960
|
-
const updated = await mcp.refreshServer(s.id);
|
|
1961
|
-
if (updated?.status === 'connected')
|
|
1962
|
-
connected++;
|
|
1963
|
-
}
|
|
1964
|
-
addMessage('system', `Refreshed ${servers.length} servers (${connected} connected)`);
|
|
1965
|
-
}
|
|
1966
|
-
else if (subCmd === 'tools') {
|
|
1967
|
-
const tools = mcp.getMCPTools();
|
|
1968
|
-
if (tools.length === 0) {
|
|
1969
|
-
addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1972
|
-
const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
|
|
1973
|
-
addMessage('system', `MCP Tools:\n\n${list}`);
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
else {
|
|
1977
|
-
addMessage('system', 'Usage: /mcp [list|add <url>|remove <id>|refresh|tools]');
|
|
1978
|
-
}
|
|
1979
|
-
break;
|
|
1980
|
-
}
|
|
1981
|
-
case '/skills': {
|
|
1982
|
-
const subCmd = parts[1];
|
|
1983
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1984
|
-
const allSkills = skills.getSkills();
|
|
1985
|
-
if (allSkills.length === 0) {
|
|
1986
|
-
addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
|
|
1987
|
-
}
|
|
1988
|
-
else {
|
|
1989
|
-
const list = allSkills.map(s => {
|
|
1990
|
-
const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
|
|
1991
|
-
return `• ${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
|
|
1992
|
-
}).join('\n\n');
|
|
1993
|
-
addMessage('system', `Installed Skills:\n\n${list}`);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
else if (subCmd === 'add' && parts[2]) {
|
|
1997
|
-
const source = parts[2];
|
|
1998
|
-
addMessage('system', `Installing skill: ${source}...`);
|
|
1999
|
-
try {
|
|
2000
|
-
let skill;
|
|
2001
|
-
if (source.startsWith('http')) {
|
|
2002
|
-
skill = await skills.installFromGithub(source);
|
|
2003
|
-
}
|
|
2004
|
-
else if (fs.existsSync(source)) {
|
|
2005
|
-
skill = skills.installLocalSkill(source);
|
|
2006
|
-
}
|
|
2007
|
-
else {
|
|
2008
|
-
skill = await skills.installFromRegistry(source);
|
|
2009
|
-
}
|
|
2010
|
-
if (skill) {
|
|
2011
|
-
addMessage('system', `✓ Installed: ${skill.metadata.name}`);
|
|
2012
|
-
}
|
|
2013
|
-
else {
|
|
2014
|
-
addMessage('error', 'Failed to install skill');
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
catch (e) {
|
|
2018
|
-
addMessage('error', `Failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
2022
|
-
if (skills.uninstallSkill(parts[2])) {
|
|
2023
|
-
addMessage('system', '✓ Skill removed');
|
|
2024
|
-
}
|
|
2025
|
-
else {
|
|
2026
|
-
addMessage('error', 'Skill not found');
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
else if (subCmd === 'info' && parts[2]) {
|
|
2030
|
-
const skill = skills.getSkill(parts[2]);
|
|
2031
|
-
if (skill) {
|
|
2032
|
-
let info = `# ${skill.metadata.name}\n\n`;
|
|
2033
|
-
info += `${skill.metadata.description}\n\n`;
|
|
2034
|
-
if (skill.metadata.compatibility)
|
|
2035
|
-
info += `Compatibility: ${skill.metadata.compatibility}\n`;
|
|
2036
|
-
if (skill.metadata.license)
|
|
2037
|
-
info += `License: ${skill.metadata.license}\n`;
|
|
2038
|
-
if (skill.sourceUrl)
|
|
2039
|
-
info += `Source: ${skill.sourceUrl}\n`;
|
|
2040
|
-
addMessage('system', info);
|
|
2041
|
-
}
|
|
2042
|
-
else {
|
|
2043
|
-
addMessage('error', 'Skill not found');
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
else {
|
|
2047
|
-
addMessage('system', 'Usage: /skills [list|add <source>|remove <name>|info <name>]');
|
|
2048
|
-
}
|
|
2049
|
-
break;
|
|
2050
|
-
}
|
|
2051
|
-
case '/memory': {
|
|
2052
|
-
const memory = await import('./memory.js');
|
|
2053
|
-
const subCmd = parts[1];
|
|
2054
|
-
const cwd = process.cwd();
|
|
2055
|
-
if (subCmd === 'init') {
|
|
2056
|
-
const memPath = memory.initProjectMemory(cwd);
|
|
2057
|
-
addMessage('system', `Created: ${memPath}\nEdit the file to add context and preferences.`);
|
|
2058
|
-
}
|
|
2059
|
-
else if (subCmd === 'show' || !subCmd) {
|
|
2060
|
-
const memPath = memory.findProjectMemory(cwd);
|
|
2061
|
-
if (!memPath) {
|
|
2062
|
-
addMessage('system', 'No CALLIOPE.md found.\nRun /memory init to create one.');
|
|
2063
|
-
}
|
|
2064
|
-
else {
|
|
2065
|
-
const mem = memory.loadMemory(memPath);
|
|
2066
|
-
let info = `Memory: ${memPath}\n\n`;
|
|
2067
|
-
if (mem.context.length)
|
|
2068
|
-
info += `**Context:**\n${mem.context.map(c => ` - ${c}`).join('\n')}\n\n`;
|
|
2069
|
-
if (mem.preferences.length)
|
|
2070
|
-
info += `**Preferences:**\n${mem.preferences.map(p => ` - ${p}`).join('\n')}\n\n`;
|
|
2071
|
-
if (mem.history.length)
|
|
2072
|
-
info += `**History:**\n${mem.history.slice(-5).map(h => ` - ${h}`).join('\n')}\n`;
|
|
2073
|
-
addMessage('system', info);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
else if (subCmd === 'add' && parts[2]) {
|
|
2077
|
-
const type = parts[2];
|
|
2078
|
-
const content = parts.slice(3).join(' ');
|
|
2079
|
-
if (!content) {
|
|
2080
|
-
addMessage('error', 'Usage: /memory add <type> <content>');
|
|
2081
|
-
}
|
|
2082
|
-
else {
|
|
2083
|
-
let memPath = memory.findProjectMemory(cwd);
|
|
2084
|
-
if (!memPath) {
|
|
2085
|
-
memPath = memory.initProjectMemory(cwd);
|
|
2086
|
-
}
|
|
2087
|
-
memory.addMemoryEntry(memPath, {
|
|
2088
|
-
type,
|
|
2089
|
-
content,
|
|
2090
|
-
timestamp: new Date().toISOString().split('T')[0],
|
|
2091
|
-
});
|
|
2092
|
-
addMessage('system', `Added ${type}: ${content}`);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
else if (subCmd === 'remove' && parts[2]) {
|
|
2096
|
-
const type = parts[2];
|
|
2097
|
-
const content = parts.slice(3).join(' ');
|
|
2098
|
-
const memPath = memory.findProjectMemory(cwd);
|
|
2099
|
-
if (memPath && memory.removeMemoryEntry(memPath, type, content)) {
|
|
2100
|
-
addMessage('system', `Removed matching ${type}`);
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
addMessage('error', 'Entry not found');
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
else if (subCmd === 'global') {
|
|
2107
|
-
const globalMem = memory.getGlobalMemory();
|
|
2108
|
-
let info = 'Global Memory:\n\n';
|
|
2109
|
-
if (globalMem.preferences.length)
|
|
2110
|
-
info += `**Preferences:**\n${globalMem.preferences.map(p => ` - ${p}`).join('\n')}\n`;
|
|
2111
|
-
if (globalMem.notes.length)
|
|
2112
|
-
info += `**Notes:**\n${globalMem.notes.map(n => ` - ${n}`).join('\n')}\n`;
|
|
2113
|
-
addMessage('system', info || 'No global memories yet.');
|
|
2114
|
-
}
|
|
2115
|
-
else {
|
|
2116
|
-
addMessage('system', 'Usage: /memory [init|show|add <type> <text>|remove <type> <text>|global]');
|
|
2117
|
-
}
|
|
2118
|
-
break;
|
|
2119
|
-
}
|
|
2120
|
-
case '/find': {
|
|
2121
|
-
const fuzzy = await import('./fuzzy-search.js');
|
|
2122
|
-
const query = parts.slice(1).join(' ');
|
|
2123
|
-
if (!query) {
|
|
2124
|
-
addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
|
|
2125
|
-
}
|
|
2126
|
-
else {
|
|
2127
|
-
const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
|
|
2128
|
-
if (results.length === 0) {
|
|
2129
|
-
addMessage('system', 'No files found');
|
|
2130
|
-
}
|
|
2131
|
-
else {
|
|
2132
|
-
const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
|
|
2133
|
-
addMessage('system', `Found ${results.length} files:\n\n${list}`);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
break;
|
|
2137
|
-
}
|
|
2138
|
-
case '/branch': {
|
|
2139
|
-
const branching = await import('./branching.js');
|
|
2140
|
-
const subCmd = parts[1];
|
|
2141
|
-
const sessionId = `session_${Date.now()}`; // Would use actual session ID
|
|
2142
|
-
if (subCmd === 'list' || !subCmd) {
|
|
2143
|
-
const tree = branching.getBranchTree(sessionId);
|
|
2144
|
-
addMessage('system', `Branches:\n${tree}`);
|
|
2145
|
-
}
|
|
2146
|
-
else if (subCmd === 'new' && parts[2]) {
|
|
2147
|
-
const branch = branching.createBranch(sessionId, parts[2], llmMessages.current, parts.slice(3).join(' '));
|
|
2148
|
-
addMessage('system', `Created branch: ${branch.name}`);
|
|
2149
|
-
}
|
|
2150
|
-
else if (subCmd === 'switch' && parts[2]) {
|
|
2151
|
-
const msgs = branching.switchBranch(sessionId, parts[2], llmMessages.current);
|
|
2152
|
-
if (msgs) {
|
|
2153
|
-
llmMessages.current = msgs;
|
|
2154
|
-
addMessage('system', `Switched to branch: ${parts[2]}`);
|
|
2155
|
-
}
|
|
2156
|
-
else {
|
|
2157
|
-
addMessage('error', 'Branch not found');
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
2161
|
-
if (branching.deleteBranch(sessionId, parts[2])) {
|
|
2162
|
-
addMessage('system', 'Branch deleted');
|
|
2163
|
-
}
|
|
2164
|
-
else {
|
|
2165
|
-
addMessage('error', 'Cannot delete branch');
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
else {
|
|
2169
|
-
addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
|
|
2170
|
-
}
|
|
2171
|
-
break;
|
|
2172
|
-
}
|
|
2173
|
-
case '/theme': {
|
|
2174
|
-
const themes = await import('./themes.js');
|
|
2175
|
-
const subCmd = parts[1];
|
|
2176
|
-
if (subCmd === 'list' || !subCmd) {
|
|
2177
|
-
const list = themes.listThemes();
|
|
2178
|
-
const current = themes.getCurrentThemeName();
|
|
2179
|
-
const formatted = list.map(t => {
|
|
2180
|
-
const marker = t.name === current ? ' *' : '';
|
|
2181
|
-
const custom = t.custom ? ' (custom)' : '';
|
|
2182
|
-
return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
|
|
2183
|
-
}).join('\n');
|
|
2184
|
-
addMessage('system', `Available themes:\n${formatted}`);
|
|
2185
|
-
}
|
|
2186
|
-
else if (themes.setCurrentTheme(subCmd)) {
|
|
2187
|
-
themes.clearThemeCache();
|
|
2188
|
-
addMessage('system', `Theme set to: ${subCmd}`);
|
|
2189
|
-
}
|
|
2190
|
-
else {
|
|
2191
|
-
addMessage('error', `Theme not found: ${subCmd}`);
|
|
2192
|
-
}
|
|
2193
|
-
break;
|
|
2194
|
-
}
|
|
2195
|
-
case '/hooks': {
|
|
2196
|
-
const hooks = await import('./hooks.js');
|
|
2197
|
-
const subCmd = parts[1];
|
|
2198
|
-
if (subCmd === 'list' || !subCmd) {
|
|
2199
|
-
addMessage('system', hooks.listHooksFormatted());
|
|
2200
|
-
}
|
|
2201
|
-
else if (subCmd === 'add' && parts[2]) {
|
|
2202
|
-
const event = parts[2];
|
|
2203
|
-
const command = parts.slice(3).join(' ');
|
|
2204
|
-
if (!command) {
|
|
2205
|
-
addMessage('system', 'Usage: /hooks add <event> <command>');
|
|
2206
|
-
}
|
|
2207
|
-
else {
|
|
2208
|
-
hooks.addHook({ event, name: `Hook for ${event}`, command, enabled: true, async: false });
|
|
2209
|
-
addMessage('system', 'Hook added');
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
else if (subCmd === 'init') {
|
|
2213
|
-
hooks.initDefaultHooks();
|
|
2214
|
-
addMessage('system', 'Default hooks initialized');
|
|
2215
|
-
}
|
|
2216
|
-
else {
|
|
2217
|
-
addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
|
|
2218
|
-
}
|
|
2219
|
-
break;
|
|
2220
|
-
}
|
|
2221
|
-
case '/search': {
|
|
2222
|
-
const query = parts.slice(1).join(' ');
|
|
2223
|
-
if (!query) {
|
|
2224
|
-
addMessage('system', 'Usage: /search <query>\nSearch conversation history');
|
|
2225
|
-
}
|
|
2226
|
-
else {
|
|
2227
|
-
const lower = query.toLowerCase();
|
|
2228
|
-
const matches = messages.filter(m => m.content.toLowerCase().includes(lower));
|
|
2229
|
-
if (matches.length === 0) {
|
|
2230
|
-
addMessage('system', 'No matches found');
|
|
2231
|
-
}
|
|
2232
|
-
else {
|
|
2233
|
-
const results = matches.slice(-10).map(m => {
|
|
2234
|
-
const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
|
|
2235
|
-
return `[${m.type}] ${preview}...`;
|
|
2236
|
-
}).join('\n\n');
|
|
2237
|
-
addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
break;
|
|
2241
|
-
}
|
|
2242
|
-
case '/project': {
|
|
2243
|
-
const projectConfig = await import('./project-config.js');
|
|
2244
|
-
const subCmd = parts[1];
|
|
2245
|
-
const cwd = process.cwd();
|
|
2246
|
-
if (subCmd === 'init') {
|
|
2247
|
-
const configPath = projectConfig.createProjectConfig(cwd);
|
|
2248
|
-
addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
|
|
2249
|
-
}
|
|
2250
|
-
else if (subCmd === 'show' || !subCmd) {
|
|
2251
|
-
const configPath = projectConfig.findProjectConfig(cwd);
|
|
2252
|
-
if (!configPath) {
|
|
2253
|
-
addMessage('system', 'No project config found.\nRun /project init to create one.');
|
|
2254
|
-
}
|
|
2255
|
-
else {
|
|
2256
|
-
const cfg = projectConfig.loadProjectConfig(configPath);
|
|
2257
|
-
if (cfg) {
|
|
2258
|
-
let info = `Config: ${configPath}\n\n`;
|
|
2259
|
-
if (cfg.project)
|
|
2260
|
-
info += `Project: ${cfg.project}\n`;
|
|
2261
|
-
if (cfg.provider)
|
|
2262
|
-
info += `Provider: ${cfg.provider}\n`;
|
|
2263
|
-
if (cfg.model)
|
|
2264
|
-
info += `Model: ${cfg.model}\n`;
|
|
2265
|
-
if (cfg.tech?.length)
|
|
2266
|
-
info += `Tech: ${cfg.tech.join(', ')}\n`;
|
|
2267
|
-
if (cfg.conventions?.length)
|
|
2268
|
-
info += `\nConventions:\n${cfg.conventions.map(c => ` - ${c}`).join('\n')}\n`;
|
|
2269
|
-
if (cfg.commands)
|
|
2270
|
-
info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
|
|
2271
|
-
addMessage('system', info);
|
|
2272
|
-
}
|
|
2273
|
-
else {
|
|
2274
|
-
addMessage('error', 'Failed to parse config');
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
else if (subCmd === 'run' && parts[2]) {
|
|
2279
|
-
const configPath = projectConfig.findProjectConfig(cwd);
|
|
2280
|
-
const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
|
|
2281
|
-
const cmdName = parts[2];
|
|
2282
|
-
if (cfg?.commands?.[cmdName]) {
|
|
2283
|
-
addMessage('system', `Running: ${cfg.commands[cmdName]}`);
|
|
2284
|
-
// Queue the command to run
|
|
2285
|
-
const { spawn } = await import('child_process');
|
|
2286
|
-
const proc = spawn('sh', ['-c', cfg.commands[cmdName]], { cwd, stdio: 'pipe' });
|
|
2287
|
-
let output = '';
|
|
2288
|
-
proc.stdout?.on('data', (d) => output += d.toString());
|
|
2289
|
-
proc.stderr?.on('data', (d) => output += d.toString());
|
|
2290
|
-
proc.on('close', (code) => {
|
|
2291
|
-
addMessage('system', `Exit ${code}\n${output}`);
|
|
2292
|
-
});
|
|
2293
|
-
}
|
|
2294
|
-
else {
|
|
2295
|
-
addMessage('error', `Command not found: ${cmdName}`);
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
else {
|
|
2299
|
-
addMessage('system', 'Usage: /project [init|show|run <cmd>]');
|
|
2300
|
-
}
|
|
2301
|
-
break;
|
|
2302
|
-
}
|
|
2303
|
-
case '/route':
|
|
2304
|
-
case '/autoroute': {
|
|
2305
|
-
if (parts[1] === 'on') {
|
|
2306
|
-
setAutoRoute(true);
|
|
2307
|
-
addMessage('system', '✓ Auto-routing ON - model selected based on task complexity');
|
|
2308
|
-
}
|
|
2309
|
-
else if (parts[1] === 'off') {
|
|
2310
|
-
setAutoRoute(false);
|
|
2311
|
-
addMessage('system', '✓ Auto-routing OFF - using fixed model');
|
|
2312
|
-
}
|
|
2313
|
-
else if (parts[1] === 'test' && parts[2]) {
|
|
2314
|
-
const testMsg = parts.slice(2).join(' ');
|
|
2315
|
-
const decision = modelRouter.routeRequest(testMsg, actualProvider);
|
|
2316
|
-
addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
|
|
2317
|
-
}
|
|
2318
|
-
else {
|
|
2319
|
-
const tiers = modelRouter.getAllTiers(actualProvider);
|
|
2320
|
-
addMessage('system', `Auto-route: ${autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
|
|
2321
|
-
}
|
|
2322
|
-
break;
|
|
2323
|
-
}
|
|
2324
|
-
case '/summarize': {
|
|
2325
|
-
const subCmd = parts[1];
|
|
2326
|
-
if (subCmd === 'context' || !subCmd) {
|
|
2327
|
-
const msgCount = llmMessages.current.length;
|
|
2328
|
-
if (msgCount < 5) {
|
|
2329
|
-
addMessage('system', 'Not enough messages to summarize.');
|
|
2330
|
-
}
|
|
2331
|
-
else {
|
|
2332
|
-
const summary = summarization.extractKeyInfo(llmMessages.current);
|
|
2333
|
-
let info = 'Context Summary:\n\n';
|
|
2334
|
-
if (summary.topics.length)
|
|
2335
|
-
info += `**Topics:** ${summary.topics.join(', ')}\n`;
|
|
2336
|
-
if (summary.decisions.length)
|
|
2337
|
-
info += `**Decisions:**\n${summary.decisions.map(d => ` - ${d}`).join('\n')}\n`;
|
|
2338
|
-
if (summary.actions.length)
|
|
2339
|
-
info += `**Actions:**\n${summary.actions.map(a => ` - ${a}`).join('\n')}\n`;
|
|
2340
|
-
if (summary.codeChanges.length)
|
|
2341
|
-
info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map(c => ` - ${c}`).join('\n')}\n`;
|
|
2342
|
-
addMessage('system', info || 'No key information extracted.');
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
else if (subCmd === 'compact') {
|
|
2346
|
-
// Summarize and compact the conversation
|
|
2347
|
-
const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 50000 });
|
|
2348
|
-
if (result.summarizedCount > 0) {
|
|
2349
|
-
llmMessages.current = result.messages;
|
|
2350
|
-
setContextTokens(estimateContextTokens());
|
|
2351
|
-
addMessage('system', `✓ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
|
|
2352
|
-
}
|
|
2353
|
-
else {
|
|
2354
|
-
addMessage('system', 'Context already within limits, no compaction needed.');
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
else {
|
|
2358
|
-
addMessage('system', 'Usage: /summarize [context|compact]');
|
|
2359
|
-
}
|
|
2360
|
-
break;
|
|
2361
|
-
}
|
|
2362
|
-
case '/upgrade':
|
|
2363
|
-
addMessage('system', 'Checking for updates...');
|
|
2364
|
-
try {
|
|
2365
|
-
const current = getVersion();
|
|
2366
|
-
const latest = await getLatestVersion();
|
|
2367
|
-
if (!latest) {
|
|
2368
|
-
addMessage('error', 'Could not check for updates');
|
|
2369
|
-
break;
|
|
2370
|
-
}
|
|
2371
|
-
const [cMaj, cMin, cPat] = current.split('.').map(Number);
|
|
2372
|
-
const [lMaj, lMin, lPat] = latest.split('.').map(Number);
|
|
2373
|
-
const hasUpdate = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
|
|
2374
|
-
if (hasUpdate) {
|
|
2375
|
-
setLatestVersion(latest);
|
|
2376
|
-
setModalMode('upgrade');
|
|
2377
|
-
}
|
|
2378
|
-
else {
|
|
2379
|
-
addMessage('system', `You're on the latest version (v${current})`);
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
catch (e) {
|
|
2383
|
-
addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
|
|
2384
|
-
}
|
|
2385
|
-
break;
|
|
2386
|
-
case '/session':
|
|
2387
|
-
case '/sessions':
|
|
2388
|
-
if (parts[1] === 'list' || !parts[1]) {
|
|
2389
|
-
const sessions = storage.listSessions(20);
|
|
2390
|
-
if (sessions.length === 0) {
|
|
2391
|
-
addMessage('system', 'No previous sessions found.');
|
|
2392
|
-
}
|
|
2393
|
-
else {
|
|
2394
|
-
setAvailableSessions(sessions);
|
|
2395
|
-
setModalMode('sessions');
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
else if (parts[1] === 'info') {
|
|
2399
|
-
const session = sessionRef.current;
|
|
2400
|
-
if (session) {
|
|
2401
|
-
addMessage('system', `Session: ${session.projectName}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}`);
|
|
2402
|
-
}
|
|
2403
|
-
else {
|
|
2404
|
-
addMessage('system', 'No active session.');
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
else {
|
|
2408
|
-
addMessage('system', 'Usage: /session [list|info] or just /sessions');
|
|
2409
|
-
}
|
|
2410
|
-
break;
|
|
2411
|
-
case '/todo': {
|
|
2412
|
-
const subCommand = parts[1];
|
|
2413
|
-
if (subCommand === 'add' && parts.length > 2) {
|
|
2414
|
-
const content = parts.slice(2).join(' ');
|
|
2415
|
-
const isGlobal = content.includes('--global');
|
|
2416
|
-
const isHigh = content.includes('--priority') && content.includes('high');
|
|
2417
|
-
const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
|
|
2418
|
-
const todo = storage.addTodo(cleanContent, {
|
|
2419
|
-
global: isGlobal,
|
|
2420
|
-
priority: isHigh ? 'high' : 'normal',
|
|
2421
|
-
});
|
|
2422
|
-
addMessage('system', `✓ TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
|
|
2423
|
-
}
|
|
2424
|
-
else if (subCommand === 'done' && parts[2]) {
|
|
2425
|
-
const id = parts[2];
|
|
2426
|
-
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
2427
|
-
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
2428
|
-
if (todo) {
|
|
2429
|
-
storage.updateTodo(todo.id, { status: 'completed' });
|
|
2430
|
-
addMessage('system', `✓ TODO #${id} marked done`);
|
|
2431
|
-
}
|
|
2432
|
-
else {
|
|
2433
|
-
addMessage('error', `TODO #${id} not found`);
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
else if (subCommand === 'list' || !subCommand) {
|
|
2437
|
-
const sessionTodos = storage.getSessionTodos();
|
|
2438
|
-
const globalTodos = storage.getGlobalTodos();
|
|
2439
|
-
const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
|
|
2440
|
-
const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
|
|
2441
|
-
if (pending.length === 0 && completed.length === 0) {
|
|
2442
|
-
addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
|
|
2443
|
-
}
|
|
2444
|
-
else {
|
|
2445
|
-
let output = '📋 TODOs:\n';
|
|
2446
|
-
if (pending.length > 0) {
|
|
2447
|
-
output += pending.map(t => ` ${t.priority === 'high' ? '!' : '□'} #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
2448
|
-
}
|
|
2449
|
-
if (completed.length > 0) {
|
|
2450
|
-
output += '\n\nCompleted:\n' + completed.map(t => ` ✓ #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
2451
|
-
}
|
|
2452
|
-
addMessage('system', output);
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
else if (subCommand === 'work' && parts[2]) {
|
|
2456
|
-
const id = parts[2];
|
|
2457
|
-
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
2458
|
-
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
2459
|
-
if (todo) {
|
|
2460
|
-
storage.setActiveTodo(todo.id);
|
|
2461
|
-
storage.updateTodo(todo.id, { status: 'in_progress' });
|
|
2462
|
-
addMessage('system', `✓ Working on: ${todo.content}\n\nTip: I'll help you complete this task. Describe what you need.`);
|
|
2463
|
-
}
|
|
2464
|
-
else {
|
|
2465
|
-
addMessage('error', `TODO #${id} not found`);
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
else if (subCommand === 'clear') {
|
|
2469
|
-
storage.setActiveTodo(null);
|
|
2470
|
-
addMessage('system', '✓ Active TODO cleared');
|
|
2471
|
-
}
|
|
2472
|
-
else {
|
|
2473
|
-
addMessage('system', 'Usage: /todo [add <task>|done <id>|work <id>|clear|list]');
|
|
2474
|
-
}
|
|
2475
|
-
break;
|
|
2476
|
-
}
|
|
2477
|
-
case '/plans': {
|
|
2478
|
-
const subCommand = parts[1];
|
|
2479
|
-
if (subCommand === 'list' || !subCommand) {
|
|
2480
|
-
const plans = storage.getPlans();
|
|
2481
|
-
if (plans.length === 0) {
|
|
2482
|
-
addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
|
|
2483
|
-
}
|
|
2484
|
-
else {
|
|
2485
|
-
const list = plans.slice(0, 5).map(p => `${p.status === 'completed' ? '✓' : '○'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
|
|
2486
|
-
addMessage('system', `📋 Plans:\n${list}`);
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
else if (subCommand === 'view' && parts[2]) {
|
|
2490
|
-
const plans = storage.getPlans();
|
|
2491
|
-
const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
2492
|
-
if (plan) {
|
|
2493
|
-
const phases = plan.phases.map(ph => ` ${ph.status === 'completed' ? '✓' : '○'} ${ph.name} (${ph.risk} risk)`).join('\n');
|
|
2494
|
-
addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
|
|
2495
|
-
}
|
|
2496
|
-
else {
|
|
2497
|
-
addMessage('error', `Plan #${parts[2]} not found`);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
else if (subCommand === 'rerun' && parts[2]) {
|
|
2501
|
-
const plans = storage.getPlans();
|
|
2502
|
-
const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
2503
|
-
if (plan) {
|
|
2504
|
-
// Reset plan status and activate
|
|
2505
|
-
plan.status = 'in_progress';
|
|
2506
|
-
plan.phases.forEach(ph => ph.status = 'pending');
|
|
2507
|
-
storage.savePlan(plan);
|
|
2508
|
-
storage.setActivePlan(plan);
|
|
2509
|
-
// Generate prompt for re-execution
|
|
2510
|
-
const phaseList = plan.phases.map(ph => `- ${ph.name}`).join('\n');
|
|
2511
|
-
const prompt = `Please help me execute this plan:\n\n**${plan.title}**\n\nPhases:\n${phaseList}\n\nStart with the first phase.`;
|
|
2512
|
-
setInput(prompt);
|
|
2513
|
-
addMessage('system', `✓ Plan loaded: ${plan.title}\nPress Enter to start execution.`);
|
|
2514
|
-
}
|
|
2515
|
-
else {
|
|
2516
|
-
addMessage('error', `Plan #${parts[2]} not found`);
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
else {
|
|
2520
|
-
addMessage('system', 'Usage: /plans [list|view <id>|rerun <id>]');
|
|
2521
|
-
}
|
|
2522
|
-
break;
|
|
2523
|
-
}
|
|
2524
|
-
case '/history': {
|
|
2525
|
-
const subCommand = parts[1];
|
|
2526
|
-
if (subCommand === 'search' && parts[2]) {
|
|
2527
|
-
const query = parts.slice(2).join(' ');
|
|
2528
|
-
const results = storage.searchChatHistory(query);
|
|
2529
|
-
if (results.length === 0) {
|
|
2530
|
-
addMessage('system', `No matches for "${query}"`);
|
|
2531
|
-
}
|
|
2532
|
-
else {
|
|
2533
|
-
const list = results.slice(-5).map(m => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
|
|
2534
|
-
addMessage('system', `🔍 Found ${results.length} matches:\n${list}`);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
else if (subCommand === 'clear') {
|
|
2538
|
-
addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
|
|
2539
|
-
}
|
|
2540
|
-
else {
|
|
2541
|
-
const history = storage.getChatHistory(5);
|
|
2542
|
-
if (history.length === 0) {
|
|
2543
|
-
addMessage('system', 'No chat history yet.');
|
|
2544
|
-
}
|
|
2545
|
-
else {
|
|
2546
|
-
const list = history.map(m => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
|
|
2547
|
-
addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
break;
|
|
2551
|
-
}
|
|
2552
|
-
case '/context': {
|
|
2553
|
-
const subCommand = parts[1];
|
|
2554
|
-
if (subCommand === 'load') {
|
|
2555
|
-
const limit = parseInt(parts[2]) || 20;
|
|
2556
|
-
const history = storage.getChatHistory(limit);
|
|
2557
|
-
if (history.length > 0) {
|
|
2558
|
-
// Load history into LLM context
|
|
2559
|
-
for (const msg of history) {
|
|
2560
|
-
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
2561
|
-
llmMessages.current.push({
|
|
2562
|
-
role: msg.role,
|
|
2563
|
-
content: msg.content,
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
addMessage('system', `✓ Loaded ${history.length} messages into context`);
|
|
2568
|
-
}
|
|
2569
|
-
else {
|
|
2570
|
-
addMessage('system', 'No history to load.');
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
else if (subCommand === 'summary' || !subCommand) {
|
|
2574
|
-
// Enhanced context summary with model limits
|
|
2575
|
-
const msgCount = llmMessages.current.length;
|
|
2576
|
-
const estTokens = estimateContextTokens();
|
|
2577
|
-
const modelLimit = getModelContextLimit(actualProvider, actualModel);
|
|
2578
|
-
const percentage = Math.round((estTokens / modelLimit) * 100);
|
|
2579
|
-
const formatK = (n) => n >= 1000 ? `${Math.round(n / 1000)}K` : String(n);
|
|
2580
|
-
let status = '🟢 Healthy';
|
|
2581
|
-
if (percentage > 90)
|
|
2582
|
-
status = '🔴 Critical';
|
|
2583
|
-
else if (percentage > 80)
|
|
2584
|
-
status = '🟡 Warning';
|
|
2585
|
-
else if (percentage > 60)
|
|
2586
|
-
status = '🟠 Caution';
|
|
2587
|
-
addMessage('system', `**Context Status: ${status}**
|
|
2588
|
-
|
|
2589
|
-
**Usage:** ${formatK(estTokens)} / ${formatK(modelLimit)} tokens (${percentage}%)
|
|
2590
|
-
**Messages:** ${msgCount}
|
|
2591
|
-
**Provider:** ${actualProvider}
|
|
2592
|
-
**Model:** ${actualModel}
|
|
2593
|
-
|
|
2594
|
-
**Commands:**
|
|
2595
|
-
/summarize compact - Auto-compress context
|
|
2596
|
-
/context load [n] - Load n messages from history
|
|
2597
|
-
/clear - Start fresh`);
|
|
2598
|
-
}
|
|
2599
|
-
else {
|
|
2600
|
-
addMessage('system', 'Usage: /context [load [n]|summary]\n\nShow context status or load history.');
|
|
2601
|
-
}
|
|
2602
|
-
break;
|
|
2603
|
-
}
|
|
2604
|
-
case '/scope':
|
|
2605
|
-
case '/dirs': {
|
|
2606
|
-
const subCmd = parts[1];
|
|
2607
|
-
if (subCmd === 'details' || subCmd === 'full') {
|
|
2608
|
-
addMessage('system', getScopeDetails());
|
|
2609
|
-
}
|
|
2610
|
-
else if (subCmd === 'reset') {
|
|
2611
|
-
resetScope(process.cwd());
|
|
2612
|
-
addMessage('system', '✓ Scope reset to current directory only');
|
|
2613
|
-
}
|
|
2614
|
-
else {
|
|
2615
|
-
addMessage('system', getScopeSummary());
|
|
2616
|
-
}
|
|
2617
|
-
break;
|
|
2618
|
-
}
|
|
2619
|
-
case '/add-dir': {
|
|
2620
|
-
const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2621
|
-
if (!dirPath) {
|
|
2622
|
-
addMessage('system', 'Usage: /add-dir <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
|
|
2623
|
-
}
|
|
2624
|
-
else {
|
|
2625
|
-
const result = addToScope(dirPath);
|
|
2626
|
-
if (result.success) {
|
|
2627
|
-
addMessage('system', `✓ ${result.message}`);
|
|
2628
|
-
}
|
|
2629
|
-
else {
|
|
2630
|
-
addMessage('error', result.message);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
break;
|
|
2634
|
-
}
|
|
2635
|
-
case '/remove-dir': {
|
|
2636
|
-
const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2637
|
-
if (!dirPath) {
|
|
2638
|
-
addMessage('system', 'Usage: /remove-dir <path>\n\nRemove a directory from the allowed scope.');
|
|
2639
|
-
}
|
|
2640
|
-
else {
|
|
2641
|
-
const result = removeFromScope(dirPath);
|
|
2642
|
-
if (result.success) {
|
|
2643
|
-
addMessage('system', `✓ ${result.message}`);
|
|
2644
|
-
}
|
|
2645
|
-
else {
|
|
2646
|
-
addMessage('error', result.message);
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
break;
|
|
2650
|
-
}
|
|
2651
|
-
case '/template':
|
|
2652
|
-
case '/t': {
|
|
2653
|
-
const subCmd = parts[1];
|
|
2654
|
-
if (subCmd === 'list' || !subCmd) {
|
|
2655
|
-
if (templates.length === 0) {
|
|
2656
|
-
addMessage('system', 'No templates saved.\n\nUsage:\n /template save <name> <prompt>\n /template use <name>\n /template delete <name>');
|
|
2657
|
-
}
|
|
2658
|
-
else {
|
|
2659
|
-
const list = templates.map((t, i) => ` ${i + 1}. ${t.name}: "${t.prompt.substring(0, 50)}${t.prompt.length > 50 ? '...' : ''}"`).join('\n');
|
|
2660
|
-
addMessage('system', `Templates:\n${list}`);
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
else if (subCmd === 'save' && parts[2]) {
|
|
2664
|
-
const name = parts[2];
|
|
2665
|
-
const prompt = parts.slice(3).join(' ').replace(/^["']|["']$/g, '');
|
|
2666
|
-
if (!prompt) {
|
|
2667
|
-
addMessage('error', 'Usage: /template save <name> "<prompt>"');
|
|
2668
|
-
}
|
|
2669
|
-
else {
|
|
2670
|
-
storage.saveTemplate(name, prompt);
|
|
2671
|
-
setTemplates(prev => {
|
|
2672
|
-
const filtered = prev.filter(t => t.name !== name);
|
|
2673
|
-
return [...filtered, { name, prompt, createdAt: new Date() }];
|
|
2674
|
-
});
|
|
2675
|
-
addMessage('system', `✓ Template saved: ${name}`);
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
else if (subCmd === 'use' && parts[2]) {
|
|
2679
|
-
const name = parts[2];
|
|
2680
|
-
const template = templates.find(t => t.name === name);
|
|
2681
|
-
if (template) {
|
|
2682
|
-
setInput(template.prompt);
|
|
2683
|
-
addMessage('system', `✓ Template loaded: ${name} (press Enter to send)`);
|
|
2684
|
-
}
|
|
2685
|
-
else {
|
|
2686
|
-
addMessage('error', `Template not found: ${name}`);
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
2690
|
-
const name = parts[2];
|
|
2691
|
-
const found = templates.find(t => t.name === name);
|
|
2692
|
-
if (found) {
|
|
2693
|
-
storage.deleteTemplate(name);
|
|
2694
|
-
setTemplates(prev => prev.filter(t => t.name !== name));
|
|
2695
|
-
addMessage('system', `✓ Template deleted: ${name}`);
|
|
2696
|
-
}
|
|
2697
|
-
else {
|
|
2698
|
-
addMessage('error', `Template not found: ${name}`);
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
else {
|
|
2702
|
-
addMessage('system', 'Usage: /template [list|save <name> <prompt>|use <name>|delete <name>]');
|
|
2703
|
-
}
|
|
2704
|
-
break;
|
|
2705
|
-
}
|
|
2706
|
-
case '/cost':
|
|
2707
|
-
case '/costs': {
|
|
2708
|
-
const subCmd = parts[1];
|
|
2709
|
-
if (subCmd === 'reset') {
|
|
2710
|
-
storage.resetCosts();
|
|
2711
|
-
addMessage('system', '✓ Cost tracking reset');
|
|
2712
|
-
}
|
|
2713
|
-
else {
|
|
2714
|
-
addMessage('system', storage.getCostSummary());
|
|
2715
|
-
}
|
|
2716
|
-
break;
|
|
2717
|
-
}
|
|
2718
|
-
case '/bookmark':
|
|
2719
|
-
case '/bm': {
|
|
2720
|
-
const subCmd = parts[1];
|
|
2721
|
-
if (!subCmd || subCmd === 'list') {
|
|
2722
|
-
// List bookmarks
|
|
2723
|
-
if (bookmarks.length === 0) {
|
|
2724
|
-
addMessage('system', 'No bookmarks. Use /bookmark "name" to create one.');
|
|
2725
|
-
}
|
|
2726
|
-
else {
|
|
2727
|
-
const list = bookmarks.map((b, i) => ` ${i + 1}. 🔖 ${b.name} (message #${b.messageIndex})`).join('\n');
|
|
2728
|
-
addMessage('system', `Bookmarks:\n${list}\n\nUse /bookmark goto <number> to jump.`);
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
else if (subCmd === 'goto' && parts[2]) {
|
|
2732
|
-
const idx = parseInt(parts[2]) - 1;
|
|
2733
|
-
if (idx >= 0 && idx < bookmarks.length) {
|
|
2734
|
-
const bm = bookmarks[idx];
|
|
2735
|
-
// Save current state for undo
|
|
2736
|
-
saveUndoState();
|
|
2737
|
-
// Restore to bookmark point
|
|
2738
|
-
setMessages(messages.slice(0, bm.messageIndex + 1));
|
|
2739
|
-
llmMessages.current = llmMessages.current.slice(0, bm.llmMessageIndex + 1);
|
|
2740
|
-
setContextTokens(estimateContextTokens());
|
|
2741
|
-
addMessage('system', `✓ Jumped to bookmark: ${bm.name}`);
|
|
2742
|
-
}
|
|
2743
|
-
else {
|
|
2744
|
-
addMessage('error', `Invalid bookmark number. Use /bookmark list to see available.`);
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
2748
|
-
const idx = parseInt(parts[2]) - 1;
|
|
2749
|
-
if (idx >= 0 && idx < bookmarks.length) {
|
|
2750
|
-
const removed = bookmarks[idx];
|
|
2751
|
-
setBookmarks(prev => prev.filter((_, i) => i !== idx));
|
|
2752
|
-
addMessage('system', `✓ Deleted bookmark: ${removed.name}`);
|
|
2753
|
-
}
|
|
2754
|
-
else {
|
|
2755
|
-
addMessage('error', 'Invalid bookmark number.');
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
else {
|
|
2759
|
-
// Create bookmark with given name
|
|
2760
|
-
const name = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2761
|
-
const bm = {
|
|
2762
|
-
id: `bm_${Date.now()}`,
|
|
2763
|
-
name,
|
|
2764
|
-
messageIndex: messages.length - 1,
|
|
2765
|
-
llmMessageIndex: llmMessages.current.length - 1,
|
|
2766
|
-
timestamp: new Date(),
|
|
2767
|
-
};
|
|
2768
|
-
setBookmarks(prev => [...prev, bm]);
|
|
2769
|
-
addMessage('system', `🔖 Bookmark created: "${name}"`);
|
|
2770
|
-
}
|
|
2771
|
-
break;
|
|
2772
|
-
}
|
|
2773
|
-
case '/queue':
|
|
2774
|
-
case '/q': {
|
|
2775
|
-
// /q is now queue, use /exit to quit
|
|
2776
|
-
if (command === '/q' && !parts[1]) {
|
|
2777
|
-
// Just /q with no args shows queue
|
|
2778
|
-
if (queuedMessages.length === 0) {
|
|
2779
|
-
addMessage('system', 'No messages queued. Type while agent is processing to queue feedback.');
|
|
2780
|
-
}
|
|
2781
|
-
else {
|
|
2782
|
-
const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m.substring(0, 60)}${m.length > 60 ? '...' : ''}`).join('\n');
|
|
2783
|
-
addMessage('system', `📨 Queued messages (${queuedMessages.length}):\n${list}\n\nUse /queue clear to remove all.`);
|
|
2784
|
-
}
|
|
2785
|
-
break;
|
|
2786
|
-
}
|
|
2787
|
-
const subCmd = parts[1];
|
|
2788
|
-
if (subCmd === 'clear') {
|
|
2789
|
-
const count = queuedMessages.length;
|
|
2790
|
-
setQueuedMessages([]);
|
|
2791
|
-
addMessage('system', `✓ Cleared ${count} queued message${count !== 1 ? 's' : ''}`);
|
|
2792
|
-
}
|
|
2793
|
-
else if (subCmd === 'show' || !subCmd) {
|
|
2794
|
-
if (queuedMessages.length === 0) {
|
|
2795
|
-
addMessage('system', 'No messages queued.');
|
|
2796
|
-
}
|
|
2797
|
-
else {
|
|
2798
|
-
const list = queuedMessages.map((m, i) => ` ${i + 1}. ${m}`).join('\n');
|
|
2799
|
-
addMessage('system', `📨 Queued messages:\n${list}`);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
else if (subCmd === 'flush') {
|
|
2803
|
-
// Force-process queued messages even if stuck
|
|
2804
|
-
if (queuedMessages.length === 0) {
|
|
2805
|
-
addMessage('system', 'No messages to flush.');
|
|
2806
|
-
}
|
|
2807
|
-
else {
|
|
2808
|
-
const queued = [...queuedMessages];
|
|
2809
|
-
setQueuedMessages([]);
|
|
2810
|
-
setIsProcessing(false); // Force reset processing state
|
|
2811
|
-
setThinkingState(null);
|
|
2812
|
-
setStreamingResponse('');
|
|
2813
|
-
addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
|
|
2814
|
-
const followUp = queued.length === 1
|
|
2815
|
-
? queued[0]
|
|
2816
|
-
: `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
|
|
2817
|
-
setTimeout(() => {
|
|
2818
|
-
setIsProcessing(true);
|
|
2819
|
-
runAgent(followUp).finally(() => {
|
|
2820
|
-
setIsProcessing(false);
|
|
2821
|
-
setThinkingState(null);
|
|
2822
|
-
setStreamingResponse('');
|
|
2823
|
-
});
|
|
2824
|
-
}, 50);
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
else {
|
|
2828
|
-
addMessage('system', 'Usage: /queue [show|clear|flush]\n\nTip: Type while agent is processing to queue follow-up messages.');
|
|
2829
|
-
}
|
|
2830
|
-
break;
|
|
2831
|
-
}
|
|
2832
|
-
case '/flush': {
|
|
2833
|
-
// Shortcut for /queue flush - force-process queued messages
|
|
2834
|
-
if (queuedMessages.length === 0) {
|
|
2835
|
-
addMessage('system', 'No messages to flush. Use /debug to see current state.');
|
|
2836
|
-
}
|
|
2837
|
-
else {
|
|
2838
|
-
const queued = [...queuedMessages];
|
|
2839
|
-
setQueuedMessages([]);
|
|
2840
|
-
setIsProcessing(false); // Force reset processing state
|
|
2841
|
-
setThinkingState(null);
|
|
2842
|
-
setStreamingResponse('');
|
|
2843
|
-
addMessage('system', `🔄 Flushing ${queued.length} queued message(s)...`);
|
|
2844
|
-
const followUp = queued.length === 1
|
|
2845
|
-
? queued[0]
|
|
2846
|
-
: `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
|
|
2847
|
-
setTimeout(() => {
|
|
2848
|
-
setIsProcessing(true);
|
|
2849
|
-
runAgent(followUp).finally(() => {
|
|
2850
|
-
setIsProcessing(false);
|
|
2851
|
-
setThinkingState(null);
|
|
2852
|
-
setStreamingResponse('');
|
|
2853
|
-
});
|
|
2854
|
-
}, 50);
|
|
2855
|
-
}
|
|
2856
|
-
break;
|
|
2857
|
-
}
|
|
2858
|
-
case '/debug': {
|
|
2859
|
-
const subCmd = parts[1];
|
|
2860
|
-
if (subCmd === 'on') {
|
|
2861
|
-
debugEnabled = true;
|
|
2862
|
-
addMessage('system', '🔍 Debug logging ON (output to stderr). Use /debug off to disable.');
|
|
2863
|
-
}
|
|
2864
|
-
else if (subCmd === 'off') {
|
|
2865
|
-
debugEnabled = false;
|
|
2866
|
-
addMessage('system', '🔍 Debug logging OFF');
|
|
2867
|
-
}
|
|
2868
|
-
else {
|
|
2869
|
-
// Show internal state for debugging stuck issues
|
|
2870
|
-
const debugInfo = [
|
|
2871
|
-
`isProcessing: ${isProcessing}`,
|
|
2872
|
-
`queuedMessages: ${queuedMessages.length}`,
|
|
2873
|
-
`modalMode: ${modalMode}`,
|
|
2874
|
-
`confirmMode: ${confirmMode}`,
|
|
2875
|
-
`loopActive: ${loopActive}`,
|
|
2876
|
-
`thinkingState: ${thinkingState ? JSON.stringify(thinkingState) : 'null'}`,
|
|
2877
|
-
`streamingResponse length: ${streamingResponse.length}`,
|
|
2878
|
-
`llmMessages count: ${llmMessages.current.length}`,
|
|
2879
|
-
`mode: ${mode}`,
|
|
2880
|
-
`debugEnabled: ${debugEnabled}`,
|
|
2881
|
-
];
|
|
2882
|
-
addMessage('system', `🔍 Debug State:\n${debugInfo.join('\n')}\n\nUse /debug on|off to toggle logging.`);
|
|
2883
|
-
}
|
|
2884
|
-
break;
|
|
2885
|
-
}
|
|
2886
|
-
case '/unstick': {
|
|
2887
|
-
// Emergency reset of processing state
|
|
2888
|
-
setIsProcessing(false);
|
|
2889
|
-
setThinkingState(null);
|
|
2890
|
-
setStreamingResponse('');
|
|
2891
|
-
setLoopActive(false);
|
|
2892
|
-
setModalMode('none');
|
|
2893
|
-
setPendingComplexPrompt(null);
|
|
2894
|
-
// Also reset to hybrid mode if stuck in plan mode
|
|
2895
|
-
if (mode === 'plan') {
|
|
2896
|
-
setMode('hybrid');
|
|
2897
|
-
addMessage('system', '🔧 Reset processing state + switched from plan to hybrid mode.');
|
|
2898
|
-
}
|
|
2899
|
-
else {
|
|
2900
|
-
addMessage('system', '🔧 Reset processing state. You can now submit new messages.');
|
|
2901
|
-
}
|
|
2902
|
-
break;
|
|
2903
|
-
}
|
|
2904
|
-
case '/keys':
|
|
2905
|
-
case '/?': {
|
|
2906
|
-
// Show keybindings modal
|
|
2907
|
-
setModalMode('keys');
|
|
2908
|
-
break;
|
|
2909
|
-
}
|
|
2910
|
-
case '/work': {
|
|
2911
|
-
// Quick shortcut to enter work mode
|
|
2912
|
-
setMode('work');
|
|
2913
|
-
addMessage('system', `Mode: ${MODE_CONFIG['work'].icon} ${MODE_CONFIG['work'].label} - ${MODE_CONFIG['work'].description}`);
|
|
2914
|
-
break;
|
|
2915
|
-
}
|
|
2916
|
-
case '/plan': {
|
|
2917
|
-
// Quick shortcut to enter plan mode
|
|
2918
|
-
setMode('plan');
|
|
2919
|
-
addMessage('system', `Mode: ${MODE_CONFIG['plan'].icon} ${MODE_CONFIG['plan'].label} - ${MODE_CONFIG['plan'].description}`);
|
|
2920
|
-
break;
|
|
2921
|
-
}
|
|
2922
|
-
case '/resume': {
|
|
2923
|
-
// Resume previous session manually
|
|
2924
|
-
const history = storage.getChatHistory(parseInt(parts[1]) || 20);
|
|
2925
|
-
if (history.length === 0) {
|
|
2926
|
-
addMessage('system', 'No previous messages to resume.');
|
|
2927
|
-
}
|
|
2928
|
-
else {
|
|
2929
|
-
for (const msg of history) {
|
|
2930
|
-
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
2931
|
-
llmMessages.current.push({
|
|
2932
|
-
role: msg.role,
|
|
2933
|
-
content: msg.content,
|
|
2934
|
-
});
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
addMessage('system', `✓ Loaded ${history.length} messages from previous session`);
|
|
2938
|
-
setContextTokens(estimateContextTokens());
|
|
2939
|
-
}
|
|
2940
|
-
break;
|
|
2941
|
-
}
|
|
2942
|
-
case '/exit':
|
|
2943
|
-
case '/quit':
|
|
2944
|
-
exit();
|
|
2945
|
-
break;
|
|
2946
|
-
default:
|
|
2947
|
-
addMessage('error', `Unknown command: ${command}. Type /help for help.`);
|
|
2948
|
-
}
|
|
2949
|
-
}, [actualProvider, actualModel, persona, stats, addMessage, exit]);
|
|
2950
|
-
// Validate and repair message history to ensure tool_use always has tool_result
|
|
2951
|
-
const validateAndRepairMessages = useCallback(() => {
|
|
2952
|
-
const messages = llmMessages.current;
|
|
2953
|
-
let repaired = false;
|
|
2954
|
-
for (let i = 0; i < messages.length; i++) {
|
|
2955
|
-
const msg = messages[i];
|
|
2956
|
-
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
2957
|
-
// Check that each tool_use has a corresponding tool_result
|
|
2958
|
-
for (const toolCall of msg.toolCalls) {
|
|
2959
|
-
const hasResult = messages.slice(i + 1).some(m => m.role === 'tool' && m.toolCallId === toolCall.id);
|
|
2960
|
-
if (!hasResult) {
|
|
2961
|
-
// Add a placeholder tool_result for the missing tool call
|
|
2962
|
-
debugLog('repair', 'Adding missing tool_result for', toolCall.id);
|
|
2963
|
-
// Find the right position to insert (right after this assistant message or after existing tool results)
|
|
2964
|
-
let insertPos = i + 1;
|
|
2965
|
-
while (insertPos < messages.length && messages[insertPos].role === 'tool') {
|
|
2966
|
-
insertPos++;
|
|
2967
|
-
}
|
|
2968
|
-
messages.splice(insertPos, 0, {
|
|
2969
|
-
role: 'tool',
|
|
2970
|
-
content: '[Error: Tool execution was interrupted. Please retry.]',
|
|
2971
|
-
toolCallId: toolCall.id,
|
|
2972
|
-
});
|
|
2973
|
-
repaired = true;
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
if (repaired) {
|
|
2979
|
-
addMessage('system', '🔧 Repaired corrupted message history (missing tool results).');
|
|
2980
|
-
}
|
|
2981
|
-
return repaired;
|
|
2982
|
-
}, [addMessage]);
|
|
2983
|
-
// Run agent with user prompt
|
|
2984
|
-
const runAgent = useCallback(async (content) => {
|
|
2985
|
-
debugLog('runAgent', 'ENTER', typeof content === 'string' ? content.substring(0, 50) : '[complex]');
|
|
2986
|
-
// Validate message history before adding new content
|
|
2987
|
-
validateAndRepairMessages();
|
|
2988
|
-
llmMessages.current.push({ role: 'user', content });
|
|
2989
|
-
setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
|
|
2990
|
-
setStreamingResponse('');
|
|
2991
|
-
// Auto-route to appropriate model based on task complexity
|
|
2992
|
-
let effectiveModel = model;
|
|
2993
|
-
if (autoRoute && typeof content === 'string') {
|
|
2994
|
-
const routeDecision = modelRouter.routeRequest(content, provider, {
|
|
2995
|
-
messageCount: stats.messageCount,
|
|
2996
|
-
hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
|
|
2997
|
-
});
|
|
2998
|
-
effectiveModel = routeDecision.model.model;
|
|
2999
|
-
if (effectiveModel !== model) {
|
|
3000
|
-
addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
const maxIterations = config.get('maxIterations');
|
|
3004
|
-
let completedNaturally = false;
|
|
3005
|
-
// Check context limit and warn if approaching capacity
|
|
3006
|
-
// Uses model's actual context length from API when available
|
|
3007
|
-
let currentContextTokens = estimateContextTokens();
|
|
3008
|
-
const modelLimit = getModelContextLimit(actualProvider, effectiveModel || actualModel);
|
|
3009
|
-
let contextPercentage = (currentContextTokens / modelLimit) * 100;
|
|
3010
|
-
// Auto-compact if we're over 95% capacity to prevent API errors
|
|
3011
|
-
if (contextPercentage > 95) {
|
|
3012
|
-
addMessage('system', `🔄 Context at ${Math.round(contextPercentage)}% - auto-compacting to prevent errors...`);
|
|
3013
|
-
const result = summarization.summarizeConversation(llmMessages.current, {
|
|
3014
|
-
maxTokens: Math.floor(modelLimit * 0.7), // Target 70% of limit after compaction
|
|
3015
|
-
preserveRecent: 15,
|
|
3016
|
-
});
|
|
3017
|
-
if (result.summarizedCount > 0) {
|
|
3018
|
-
llmMessages.current = result.messages;
|
|
3019
|
-
currentContextTokens = estimateContextTokens();
|
|
3020
|
-
contextPercentage = (currentContextTokens / modelLimit) * 100;
|
|
3021
|
-
setContextTokens(currentContextTokens);
|
|
3022
|
-
addMessage('system', `✓ Compacted ${result.summarizedCount} messages. Now at ${Math.round(contextPercentage)}% (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K)`);
|
|
3023
|
-
}
|
|
3024
|
-
else {
|
|
3025
|
-
// If compaction didn't help enough, warn user
|
|
3026
|
-
if (contextPercentage > 98) {
|
|
3027
|
-
addMessage('error', `🚨 Context at ${Math.round(contextPercentage)}% - cannot proceed safely. Please use /clear or reduce message size.`);
|
|
3028
|
-
setIsProcessing(false);
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
else if (contextPercentage > 85) {
|
|
3034
|
-
addMessage('system', `⚠️ Context at ${Math.round(contextPercentage)}% capacity (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K tokens)
|
|
3035
|
-
Consider: /summarize compact | /clear | shorter messages`);
|
|
3036
|
-
}
|
|
3037
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
3038
|
-
// Safety check at start of each iteration - context may have grown from tool results
|
|
3039
|
-
if (i > 0) {
|
|
3040
|
-
const iterContextTokens = estimateContextTokens();
|
|
3041
|
-
const iterContextPercentage = (iterContextTokens / modelLimit) * 100;
|
|
3042
|
-
if (iterContextPercentage > 95) {
|
|
3043
|
-
addMessage('system', `🔄 Context grew to ${Math.round(iterContextPercentage)}% - auto-compacting...`);
|
|
3044
|
-
const result = summarization.summarizeConversation(llmMessages.current, {
|
|
3045
|
-
maxTokens: Math.floor(modelLimit * 0.7),
|
|
3046
|
-
preserveRecent: 15,
|
|
3047
|
-
});
|
|
3048
|
-
if (result.summarizedCount > 0) {
|
|
3049
|
-
llmMessages.current = result.messages;
|
|
3050
|
-
setContextTokens(estimateContextTokens());
|
|
3051
|
-
addMessage('system', `✓ Compacted ${result.summarizedCount} messages during iteration ${i + 1}`);
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
try {
|
|
3056
|
-
// Update thinking state for LLM call
|
|
3057
|
-
setThinkingState({
|
|
3058
|
-
status: i === 0 ? 'Analyzing request...' : 'Processing response...',
|
|
3059
|
-
detail: `Iteration ${i + 1}/${maxIterations}`,
|
|
3060
|
-
iteration: i + 1,
|
|
3061
|
-
maxIterations,
|
|
3062
|
-
});
|
|
3063
|
-
setActivityState({
|
|
3064
|
-
action: i === 0 ? 'Analyzing request' : 'Processing',
|
|
3065
|
-
target: `iteration ${i + 1}`,
|
|
3066
|
-
startTime: Date.now(),
|
|
3067
|
-
});
|
|
3068
|
-
// Streaming callback for final response
|
|
3069
|
-
const onToken = (token) => {
|
|
3070
|
-
setThinkingState(null); // Clear thinking when streaming starts
|
|
3071
|
-
setStreamingResponse(prev => prev + token);
|
|
3072
|
-
};
|
|
3073
|
-
// Retry callback for error recovery
|
|
3074
|
-
const onRetry = (attempt, error, delayMs) => {
|
|
3075
|
-
setThinkingState({
|
|
3076
|
-
status: `Retrying... (attempt ${attempt + 1})`,
|
|
3077
|
-
detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
|
|
3078
|
-
iteration: i + 1,
|
|
3079
|
-
maxIterations,
|
|
3080
|
-
});
|
|
3081
|
-
};
|
|
3082
|
-
debugLog('chat', 'WAITING for LLM response', `iteration=${i + 1}`);
|
|
3083
|
-
// Validate message history to prevent orphaned tool_result errors
|
|
3084
|
-
let validatedMessages = summarization.validateMessageHistory(llmMessages.current);
|
|
3085
|
-
if (validatedMessages.length !== llmMessages.current.length) {
|
|
3086
|
-
debugLog('chat', 'CLEANED orphaned tool results', `removed=${llmMessages.current.length - validatedMessages.length}`);
|
|
3087
|
-
llmMessages.current = validatedMessages;
|
|
3088
|
-
}
|
|
3089
|
-
// Pre-request summarization check - summarize BEFORE sending if context is too large
|
|
3090
|
-
const tools = getTools(moduleAgtermEnabled);
|
|
3091
|
-
const contextCheck = estimateContextUsage(provider, effectiveModel || DEFAULT_MODELS[provider], validatedMessages, tools);
|
|
3092
|
-
debugLog('chat', 'CONTEXT CHECK', `estimated=${contextCheck.estimated}, limit=${contextCheck.limit}, percent=${contextCheck.percent}%`);
|
|
3093
|
-
if (contextCheck.needsSummarization) {
|
|
3094
|
-
debugLog('chat', 'PRE-REQUEST SUMMARIZING', `estimated=${contextCheck.estimated} >= 80% of ${contextCheck.limit}`);
|
|
3095
|
-
const result = summarization.summarizeConversation(validatedMessages, { maxTokens: Math.floor(contextCheck.limit * 0.6) });
|
|
3096
|
-
if (result.summarizedCount > 0) {
|
|
3097
|
-
llmMessages.current = result.messages;
|
|
3098
|
-
validatedMessages = result.messages;
|
|
3099
|
-
debugLog('chat', 'PRE-SUMMARIZED', `removed=${result.summarizedCount} messages, reduced from ${result.originalTokens} to ${result.reducedTokens}`);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
const response = await chat(provider, validatedMessages, tools, effectiveModel, onToken, onRetry);
|
|
3103
|
-
debugLog('chat', 'GOT response', `toolCalls=${response.toolCalls?.length ?? 0}`);
|
|
3104
|
-
// Update token stats and cost
|
|
3105
|
-
if (response.usage) {
|
|
3106
|
-
const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
|
|
3107
|
-
setStats(s => ({
|
|
3108
|
-
...s,
|
|
3109
|
-
inputTokens: s.inputTokens + response.usage.inputTokens,
|
|
3110
|
-
outputTokens: s.outputTokens + response.usage.outputTokens,
|
|
3111
|
-
cost: s.cost + usageCost,
|
|
3112
|
-
}));
|
|
3113
|
-
// Persist cost to storage
|
|
3114
|
-
storage.recordCost(usageCost, actualProvider, sessionRef.current?.id);
|
|
3115
|
-
// Auto-summarize if context is getting too full (85% threshold)
|
|
3116
|
-
if (needsSummarization(provider, model || DEFAULT_MODELS[provider], response.usage.inputTokens)) {
|
|
3117
|
-
debugLog('chat', 'AUTO-SUMMARIZING', `inputTokens=${response.usage.inputTokens}`);
|
|
3118
|
-
const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 100000 });
|
|
3119
|
-
if (result.summarizedCount > 0) {
|
|
3120
|
-
llmMessages.current = result.messages;
|
|
3121
|
-
debugLog('chat', 'SUMMARIZED', `removed=${result.summarizedCount} messages`);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
// Handle tool calls with parallel execution support
|
|
3126
|
-
if (response.toolCalls?.length) {
|
|
3127
|
-
llmMessages.current.push({
|
|
3128
|
-
role: 'assistant',
|
|
3129
|
-
content: response.content,
|
|
3130
|
-
toolCalls: response.toolCalls,
|
|
3131
|
-
});
|
|
3132
|
-
const preChecks = [];
|
|
3133
|
-
const executableTools = [];
|
|
3134
|
-
for (const toolCall of response.toolCalls) {
|
|
3135
|
-
const args = toolCall.arguments;
|
|
3136
|
-
const toolPreview = String(args.command || args.path || '...');
|
|
3137
|
-
const risk = assessToolRisk(toolCall);
|
|
3138
|
-
const riskConfig = RISK_CONFIG[risk.level];
|
|
3139
|
-
const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
|
|
3140
|
-
const preCheck = {
|
|
3141
|
-
toolCall,
|
|
3142
|
-
args,
|
|
3143
|
-
preview: toolPreview,
|
|
3144
|
-
risk,
|
|
3145
|
-
riskDisplay,
|
|
3146
|
-
blocked: false,
|
|
3147
|
-
};
|
|
3148
|
-
// Check blocking conditions
|
|
3149
|
-
if (mode === 'plan' && toolCall.name !== 'think') {
|
|
3150
|
-
preCheck.blocked = true;
|
|
3151
|
-
preCheck.blockReason = 'plan mode';
|
|
3152
|
-
preCheck.blockContent = '[Plan mode: Tool not executed. Describe what this would do.]';
|
|
3153
|
-
addMessage('tool', `📋 ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
|
|
3154
|
-
}
|
|
3155
|
-
else if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
|
|
3156
|
-
preCheck.blocked = true;
|
|
3157
|
-
preCheck.blockReason = 'confirmation required';
|
|
3158
|
-
preCheck.blockContent = `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`;
|
|
3159
|
-
const riskIcon = risk.level === 'critical' ? '🛑' : '⚠️';
|
|
3160
|
-
addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
|
|
3161
|
-
}
|
|
3162
|
-
else {
|
|
3163
|
-
// Check pre-tool hooks
|
|
3164
|
-
const preHookResult = await hooks.checkHooksAllow('pre-tool', {
|
|
3165
|
-
tool: toolCall.name,
|
|
3166
|
-
toolArgs: args,
|
|
3167
|
-
});
|
|
3168
|
-
if (!preHookResult.allowed) {
|
|
3169
|
-
preCheck.blocked = true;
|
|
3170
|
-
preCheck.blockReason = 'blocked by hook';
|
|
3171
|
-
preCheck.blockContent = `[Blocked by hook: ${preHookResult.reason}]`;
|
|
3172
|
-
addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
|
|
3173
|
-
addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
|
|
3174
|
-
}
|
|
3175
|
-
else {
|
|
3176
|
-
// Tool can be executed
|
|
3177
|
-
executableTools.push(toolCall);
|
|
3178
|
-
addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
preChecks.push(preCheck);
|
|
3182
|
-
// Add blocked tool results to LLM messages
|
|
3183
|
-
if (preCheck.blocked) {
|
|
3184
|
-
llmMessages.current.push({
|
|
3185
|
-
role: 'tool',
|
|
3186
|
-
content: preCheck.blockContent,
|
|
3187
|
-
toolCallId: toolCall.id,
|
|
3188
|
-
});
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
// ============================================================
|
|
3192
|
-
// Phase 2: Execute tools (parallel when beneficial)
|
|
3193
|
-
// ============================================================
|
|
3194
|
-
if (executableTools.length > 0) {
|
|
3195
|
-
const parallelStats = getParallelizationStats(executableTools);
|
|
3196
|
-
const useParallel = parallelStats.maxParallel > 1 && executableTools.length > 1;
|
|
3197
|
-
if (useParallel) {
|
|
3198
|
-
// Show parallelization info
|
|
3199
|
-
setThinkingState({
|
|
3200
|
-
status: `Executing ${executableTools.length} tools in parallel...`,
|
|
3201
|
-
detail: `${parallelStats.stages} stages, up to ${parallelStats.maxParallel}x speedup`,
|
|
3202
|
-
iteration: i + 1,
|
|
3203
|
-
maxIterations,
|
|
3204
|
-
});
|
|
3205
|
-
setActivityState({
|
|
3206
|
-
action: `Executing ${executableTools.length} tools`,
|
|
3207
|
-
target: 'in parallel',
|
|
3208
|
-
startTime: Date.now(),
|
|
3209
|
-
});
|
|
3210
|
-
// Execute in parallel using dependency-aware staging
|
|
3211
|
-
debugLog('tools', 'PARALLEL exec start', `count=${executableTools.length}`);
|
|
3212
|
-
const results = await executeParallel(executableTools, async (call) => {
|
|
3213
|
-
const result = await executeTool(call, process.cwd());
|
|
3214
|
-
return result.result;
|
|
3215
|
-
}, (completed, total, current) => {
|
|
3216
|
-
const args = current.arguments;
|
|
3217
|
-
const target = args.path || args.command?.substring(0, 30) || current.name;
|
|
3218
|
-
setActivityState({
|
|
3219
|
-
action: `Running ${current.name}`,
|
|
3220
|
-
target: target,
|
|
3221
|
-
startTime: Date.now(),
|
|
3222
|
-
});
|
|
3223
|
-
setThinkingState({
|
|
3224
|
-
status: `Executing tools... (${completed + 1}/${total})`,
|
|
3225
|
-
detail: current.name,
|
|
3226
|
-
iteration: i + 1,
|
|
3227
|
-
maxIterations,
|
|
3228
|
-
});
|
|
3229
|
-
});
|
|
3230
|
-
debugLog('tools', 'PARALLEL exec done', `results=${results.length}`);
|
|
3231
|
-
// Process results sequentially for UI and LLM messages
|
|
3232
|
-
for (const result of results) {
|
|
3233
|
-
const toolCall = result.toolCall;
|
|
3234
|
-
const args = toolCall.arguments;
|
|
3235
|
-
// Execute post-tool hooks
|
|
3236
|
-
hooks.executeHooks('post-tool', {
|
|
3237
|
-
tool: toolCall.name,
|
|
3238
|
-
toolArgs: args,
|
|
3239
|
-
toolResult: result.result,
|
|
3240
|
-
}).catch((err) => {
|
|
3241
|
-
debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
|
|
3242
|
-
});
|
|
3243
|
-
// Display result
|
|
3244
|
-
if (toolCall.name === 'think') {
|
|
3245
|
-
const thought = String(args.thought || '');
|
|
3246
|
-
addMessage('tool', thought);
|
|
3247
|
-
}
|
|
3248
|
-
else if (result.error) {
|
|
3249
|
-
addMessage('tool', `Error: ${result.error}`);
|
|
3250
|
-
}
|
|
3251
|
-
else {
|
|
3252
|
-
const preview = result.result.split('\n').slice(0, 3).join('\n');
|
|
3253
|
-
addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
|
|
3254
|
-
}
|
|
3255
|
-
llmMessages.current.push({
|
|
3256
|
-
role: 'tool',
|
|
3257
|
-
content: result.error ? `Error: ${result.error}` : result.result,
|
|
3258
|
-
toolCallId: toolCall.id,
|
|
3259
|
-
});
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
else {
|
|
3263
|
-
// Sequential execution (single tool or dependencies prevent parallelization)
|
|
3264
|
-
debugLog('tools', 'SEQUENTIAL exec start', `count=${executableTools.length}`);
|
|
3265
|
-
for (const toolCall of executableTools) {
|
|
3266
|
-
const args = toolCall.arguments;
|
|
3267
|
-
const toolPreview = String(args.command || args.path || args.content?.toString().substring(0, 30) || '...');
|
|
3268
|
-
// Set activity state for streaming indicator
|
|
3269
|
-
const actionMap = {
|
|
3270
|
-
read_file: 'Reading',
|
|
3271
|
-
write_file: 'Writing',
|
|
3272
|
-
edit_file: 'Editing',
|
|
3273
|
-
bash: 'Running',
|
|
3274
|
-
search: 'Searching',
|
|
3275
|
-
glob: 'Finding',
|
|
3276
|
-
think: 'Thinking',
|
|
3277
|
-
};
|
|
3278
|
-
const action = actionMap[toolCall.name] || `Executing ${toolCall.name}`;
|
|
3279
|
-
const target = toolCall.name === 'bash'
|
|
3280
|
-
? args.command?.substring(0, 40) + (args.command?.length > 40 ? '...' : '')
|
|
3281
|
-
: toolCall.name === 'think'
|
|
3282
|
-
? undefined
|
|
3283
|
-
: args.path || args.pattern;
|
|
3284
|
-
setActivityState({ action, target, startTime: Date.now() });
|
|
3285
|
-
// Special handling for think tool UI
|
|
3286
|
-
if (toolCall.name === 'think') {
|
|
3287
|
-
const thought = String(args.thought || '');
|
|
3288
|
-
setThinkingState({
|
|
3289
|
-
status: 'Reasoning...',
|
|
3290
|
-
detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
|
|
3291
|
-
thinking: thought,
|
|
3292
|
-
iteration: i + 1,
|
|
3293
|
-
maxIterations,
|
|
3294
|
-
});
|
|
3295
|
-
}
|
|
3296
|
-
else {
|
|
3297
|
-
setThinkingState({
|
|
3298
|
-
status: `Executing ${toolCall.name}...`,
|
|
3299
|
-
detail: toolPreview.substring(0, 60),
|
|
3300
|
-
thinking: undefined,
|
|
3301
|
-
iteration: i + 1,
|
|
3302
|
-
maxIterations,
|
|
3303
|
-
});
|
|
3304
|
-
}
|
|
3305
|
-
debugLog('tools', 'EXEC', toolCall.name, toolPreview.substring(0, 30));
|
|
3306
|
-
const result = await executeTool(toolCall, process.cwd());
|
|
3307
|
-
debugLog('tools', 'DONE', toolCall.name);
|
|
3308
|
-
// Execute post-tool hooks
|
|
3309
|
-
hooks.executeHooks('post-tool', {
|
|
3310
|
-
tool: toolCall.name,
|
|
3311
|
-
toolArgs: args,
|
|
3312
|
-
toolResult: result.result,
|
|
3313
|
-
}).catch((err) => {
|
|
3314
|
-
debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
|
|
3315
|
-
});
|
|
3316
|
-
// Display result
|
|
3317
|
-
if (toolCall.name === 'think') {
|
|
3318
|
-
const thought = String(args.thought || '');
|
|
3319
|
-
addMessage('tool', thought);
|
|
3320
|
-
}
|
|
3321
|
-
else {
|
|
3322
|
-
const preview = result.result.split('\n').slice(0, 3).join('\n');
|
|
3323
|
-
addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
|
|
3324
|
-
}
|
|
3325
|
-
llmMessages.current.push({
|
|
3326
|
-
role: 'tool',
|
|
3327
|
-
content: result.result,
|
|
3328
|
-
toolCallId: toolCall.id,
|
|
3329
|
-
});
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
continue;
|
|
3334
|
-
}
|
|
3335
|
-
// Final response - move streaming content to message history
|
|
3336
|
-
setThinkingState(null);
|
|
3337
|
-
llmMessages.current.push({ role: 'assistant', content: response.content });
|
|
3338
|
-
addMessage('assistant', response.content);
|
|
3339
|
-
setStreamingResponse('');
|
|
3340
|
-
setContextTokens(estimateContextTokens());
|
|
3341
|
-
checkAndWarnContextLimit(actualProvider, actualModel, estimateContextTokens(), addMessage);
|
|
3342
|
-
// Auto-continue if response was truncated due to length
|
|
3343
|
-
if (response.finishReason === 'length') {
|
|
3344
|
-
addMessage('system', '(auto-continuing...)');
|
|
3345
|
-
llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
|
|
3346
|
-
continue; // Loop again to get continuation
|
|
3347
|
-
}
|
|
3348
|
-
completedNaturally = true;
|
|
3349
|
-
break;
|
|
3350
|
-
}
|
|
3351
|
-
catch (error) {
|
|
3352
|
-
setThinkingState(null);
|
|
3353
|
-
setActivityState(null);
|
|
3354
|
-
setStreamingResponse('');
|
|
3355
|
-
// Format error with provider context for better suggestions
|
|
3356
|
-
const errorMsg = formatError(error, { provider: actualProvider });
|
|
3357
|
-
addMessage('error', errorMsg);
|
|
3358
|
-
// Classify error to provide additional recovery suggestions
|
|
3359
|
-
const classified = classifyError(error);
|
|
3360
|
-
const availableProviders = getAvailableProviders();
|
|
3361
|
-
const otherProviders = availableProviders.filter(p => p !== actualProvider);
|
|
3362
|
-
// Suggest alternatives based on error type
|
|
3363
|
-
if (classified.category === 'rate_limit' || classified.category === 'server') {
|
|
3364
|
-
if (otherProviders.length > 0) {
|
|
3365
|
-
addMessage('system', `💡 Try switching providers: /provider ${otherProviders[0]} or /models to see alternatives`);
|
|
3366
|
-
}
|
|
3367
|
-
}
|
|
3368
|
-
else if (classified.category === 'timeout' || classified.category === 'network') {
|
|
3369
|
-
addMessage('system', `💡 Network issue detected. Check connection and try again, or use /provider to switch.`);
|
|
3370
|
-
}
|
|
3371
|
-
else if (classified.category === 'auth') {
|
|
3372
|
-
addMessage('system', `💡 Run 'calliope --setup' to reconfigure API keys.`);
|
|
3373
|
-
}
|
|
3374
|
-
completedNaturally = true; // Error counts as "done" - don't show iteration warning
|
|
3375
|
-
// On error, clear queued messages to prevent infinite retry loop
|
|
3376
|
-
if (queuedMessages.length > 0) {
|
|
3377
|
-
addMessage('system', `⚠️ Cleared ${queuedMessages.length} queued message(s) due to error. Use /clear to reset conversation.`);
|
|
3378
|
-
setQueuedMessages([]);
|
|
3379
|
-
}
|
|
3380
|
-
return; // Exit early on error - don't process queued messages
|
|
3381
|
-
}
|
|
3382
|
-
}
|
|
3383
|
-
// Only show warning if we actually hit the iteration limit (not errors or natural completion)
|
|
3384
|
-
if (!completedNaturally) {
|
|
3385
|
-
addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /set maxIterations <number>.`);
|
|
3386
|
-
}
|
|
3387
|
-
// Update context tokens after agent run
|
|
3388
|
-
setContextTokens(estimateContextTokens());
|
|
3389
|
-
// Process any queued messages (human-in-the-loop feedback)
|
|
3390
|
-
// CRITICAL: Use ref to get current value, not stale closure
|
|
3391
|
-
const currentQueued = queuedMessagesRef.current;
|
|
3392
|
-
debugLog('runAgent', 'EXIT loop', `queued=${currentQueued.length}`);
|
|
3393
|
-
if (currentQueued.length > 0) {
|
|
3394
|
-
const queued = [...currentQueued];
|
|
3395
|
-
setQueuedMessages([]); // Clear the queue
|
|
3396
|
-
queuedMessagesRef.current = []; // Also clear ref immediately
|
|
3397
|
-
// Combine queued messages into a single follow-up
|
|
3398
|
-
const followUp = queued.length === 1
|
|
3399
|
-
? queued[0]
|
|
3400
|
-
: `[Multiple follow-up messages from user:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
|
|
3401
|
-
addMessage('system', `📨 Processing ${queued.length} queued message${queued.length > 1 ? 's' : ''}...`);
|
|
3402
|
-
// Recursively run agent with follow-up
|
|
3403
|
-
// Use setTimeout to avoid stack overflow and allow UI to update
|
|
3404
|
-
// Note: handleSubmit's finally will set isProcessing=false, so we need to re-enable it
|
|
3405
|
-
debugLog('runAgent', 'SCHEDULING recursive call for queued messages');
|
|
3406
|
-
setTimeout(() => {
|
|
3407
|
-
debugLog('runAgent', 'RECURSIVE call starting');
|
|
3408
|
-
setIsProcessing(true);
|
|
3409
|
-
runAgent(followUp).finally(() => {
|
|
3410
|
-
setIsProcessing(false);
|
|
3411
|
-
setThinkingState(null);
|
|
3412
|
-
setActivityState(null);
|
|
3413
|
-
setStreamingResponse('');
|
|
3414
|
-
setEditingQueueIndex(null);
|
|
3415
|
-
});
|
|
3416
|
-
}, 100);
|
|
3417
|
-
}
|
|
3418
|
-
debugLog('runAgent', 'RETURN');
|
|
3419
|
-
}, [provider, model, addMessage, mode, estimateContextTokens]); // Note: queuedMessages accessed via ref
|
|
3420
|
-
// Ralph Wiggum loop - runs prompt repeatedly until completion promise or max iterations
|
|
3421
|
-
const runLoop = useCallback(async (prompt, maxIter, completionPromise) => {
|
|
3422
|
-
setIsProcessing(true);
|
|
3423
|
-
for (let i = 0; i < maxIter; i++) {
|
|
3424
|
-
// Check if cancelled
|
|
3425
|
-
if (loopCancelledRef.current) {
|
|
3426
|
-
addMessage('system', '🛑 Loop cancelled by user');
|
|
3427
|
-
break;
|
|
3428
|
-
}
|
|
3429
|
-
setLoopIteration(i + 1);
|
|
3430
|
-
addMessage('system', `🔄 Loop iteration ${i + 1}/${maxIter}`);
|
|
3431
|
-
// Add the loop prompt as user message
|
|
3432
|
-
llmMessages.current.push({ role: 'user', content: prompt });
|
|
3433
|
-
try {
|
|
3434
|
-
// Run the agent
|
|
3435
|
-
await runAgent(prompt);
|
|
3436
|
-
// Check for completion promise in the last assistant message
|
|
3437
|
-
if (completionPromise) {
|
|
3438
|
-
const lastMessage = llmMessages.current[llmMessages.current.length - 1];
|
|
3439
|
-
if (lastMessage?.role === 'assistant') {
|
|
3440
|
-
const content = typeof lastMessage.content === 'string'
|
|
3441
|
-
? lastMessage.content
|
|
3442
|
-
: JSON.stringify(lastMessage.content);
|
|
3443
|
-
if (content.includes(completionPromise)) {
|
|
3444
|
-
addMessage('system', `🎉 Completion promise "${completionPromise}" detected! Loop finished.`);
|
|
3445
|
-
break;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
// Check cancelled again after agent run
|
|
3450
|
-
if (loopCancelledRef.current) {
|
|
3451
|
-
addMessage('system', '🛑 Loop cancelled by user');
|
|
3452
|
-
break;
|
|
3453
|
-
}
|
|
3454
|
-
// Small delay between iterations
|
|
3455
|
-
await new Promise(r => setTimeout(r, 500));
|
|
3456
|
-
}
|
|
3457
|
-
catch (error) {
|
|
3458
|
-
addMessage('error', `Loop error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3459
|
-
break;
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
// If we completed all iterations without hitting completion promise
|
|
3463
|
-
if (!loopCancelledRef.current && !completionPromise) {
|
|
3464
|
-
addMessage('system', `✅ Loop completed ${maxIter} iterations`);
|
|
3465
|
-
}
|
|
3466
|
-
setLoopActive(false);
|
|
3467
|
-
setIsProcessing(false);
|
|
3468
|
-
}, [runAgent, addMessage]);
|
|
3469
|
-
// Handle input submission
|
|
3470
|
-
const handleSubmit = useCallback(async (value) => {
|
|
3471
|
-
const trimmed = value.trim();
|
|
3472
|
-
if (!trimmed || isProcessing)
|
|
3473
|
-
return;
|
|
3474
|
-
// Add to history for up/down arrow navigation
|
|
3475
|
-
addToHistory(trimmed);
|
|
3476
|
-
setInput('');
|
|
3477
|
-
if (trimmed.startsWith('/')) {
|
|
3478
|
-
await handleCommand(trimmed);
|
|
3479
|
-
return;
|
|
3480
|
-
}
|
|
3481
|
-
// In hybrid mode, check for complex operations
|
|
3482
|
-
if (mode === 'hybrid') {
|
|
3483
|
-
const complexity = detectComplexity(trimmed);
|
|
3484
|
-
if (complexity.isComplex) {
|
|
3485
|
-
setPendingComplexPrompt({ prompt: trimmed, complexity });
|
|
3486
|
-
setModalMode('complexity-warning');
|
|
3487
|
-
return;
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
// Save state for undo before modifying conversation
|
|
3491
|
-
saveUndoState();
|
|
3492
|
-
// Parse file references from input
|
|
3493
|
-
const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
|
|
3494
|
-
// Show user message (with file info if any)
|
|
3495
|
-
if (files.length > 0) {
|
|
3496
|
-
const fileInfo = formatFileInfo(files);
|
|
3497
|
-
addMessage('user', `${cleanText}\n📎 ${fileInfo}`);
|
|
3498
|
-
}
|
|
3499
|
-
else {
|
|
3500
|
-
addMessage('user', trimmed);
|
|
3501
|
-
}
|
|
3502
|
-
setIsProcessing(true);
|
|
3503
|
-
try {
|
|
3504
|
-
// Build message content (with file/image support)
|
|
3505
|
-
let messageContent;
|
|
3506
|
-
if (files.length > 0) {
|
|
3507
|
-
const visionSupported = supportsVision(provider, model);
|
|
3508
|
-
const { content, warnings } = processFilesForMessage(cleanText || trimmed, files, visionSupported);
|
|
3509
|
-
// Show any warnings about files
|
|
3510
|
-
for (const warning of warnings) {
|
|
3511
|
-
addMessage('system', warning);
|
|
3512
|
-
}
|
|
3513
|
-
messageContent = content;
|
|
3514
|
-
}
|
|
3515
|
-
else {
|
|
3516
|
-
messageContent = trimmed;
|
|
3517
|
-
}
|
|
3518
|
-
await runAgent(messageContent);
|
|
3519
|
-
}
|
|
3520
|
-
finally {
|
|
3521
|
-
setIsProcessing(false);
|
|
3522
|
-
setThinkingState(null);
|
|
3523
|
-
setStreamingResponse('');
|
|
3524
|
-
}
|
|
3525
|
-
}, [isProcessing, handleCommand, runAgent, addMessage, provider, model, saveUndoState, addToHistory]);
|
|
3526
|
-
// Modal handlers
|
|
3527
|
-
const handleModelSelect = useCallback((selectedModel) => {
|
|
3528
|
-
setModel(selectedModel);
|
|
3529
|
-
addMessage('system', `Model: ${selectedModel}`);
|
|
3530
|
-
setModalMode('none');
|
|
3531
|
-
setAvailableModels([]);
|
|
3532
|
-
}, [addMessage]);
|
|
3533
|
-
const handleModalCancel = useCallback(() => {
|
|
3534
|
-
setModalMode('none');
|
|
3535
|
-
setAvailableModels([]);
|
|
3536
|
-
setLatestVersion(null);
|
|
3537
|
-
}, []);
|
|
3538
|
-
const handleUpgradeConfirm = useCallback(async () => {
|
|
3539
|
-
setModalMode('none');
|
|
3540
|
-
addMessage('system', 'Upgrading...');
|
|
3541
|
-
try {
|
|
3542
|
-
const success = await performUpgrade();
|
|
3543
|
-
if (success) {
|
|
3544
|
-
addMessage('system', 'Upgrade complete! Restarting...');
|
|
3545
|
-
const { spawn } = await import('child_process');
|
|
3546
|
-
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
3547
|
-
stdio: 'inherit',
|
|
3548
|
-
detached: true,
|
|
3549
|
-
});
|
|
3550
|
-
child.unref();
|
|
3551
|
-
process.exit(0);
|
|
3552
|
-
}
|
|
3553
|
-
else {
|
|
3554
|
-
addMessage('error', 'Upgrade failed. Try: npm install -g @calliopelabs/cli@latest');
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
catch (e) {
|
|
3558
|
-
addMessage('error', `Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3559
|
-
}
|
|
3560
|
-
setLatestVersion(null);
|
|
3561
|
-
}, [addMessage]);
|
|
3562
|
-
// Cycle through modes
|
|
3563
|
-
const cycleMode = useCallback(() => {
|
|
3564
|
-
setMode(current => {
|
|
3565
|
-
const modes = ['plan', 'hybrid', 'work'];
|
|
3566
|
-
const idx = modes.indexOf(current);
|
|
3567
|
-
const next = modes[(idx + 1) % modes.length];
|
|
3568
|
-
return next;
|
|
3569
|
-
});
|
|
3570
|
-
}, []);
|
|
3571
|
-
// Handle Escape key - cancel operation if processing, otherwise show hint
|
|
3572
|
-
const handleEscape = useCallback(() => {
|
|
3573
|
-
if (isProcessing) {
|
|
3574
|
-
// Cancel current operation
|
|
3575
|
-
setIsProcessing(false);
|
|
3576
|
-
setThinkingState(null);
|
|
3577
|
-
setStreamingResponse('');
|
|
3578
|
-
setLoopActive(false);
|
|
3579
|
-
setEditingQueueIndex(null);
|
|
3580
|
-
addMessage('system', '⏹ Operation cancelled. Use /exit to quit.');
|
|
3581
|
-
}
|
|
3582
|
-
else if (modalMode !== 'none') {
|
|
3583
|
-
// Close any open modal
|
|
3584
|
-
setModalMode('none');
|
|
3585
|
-
setPendingComplexPrompt(null);
|
|
3586
|
-
}
|
|
3587
|
-
else {
|
|
3588
|
-
// Not processing - show hint instead of exiting
|
|
3589
|
-
addMessage('system', '💡 Use /exit to quit, or Ctrl+C.');
|
|
3590
|
-
}
|
|
3591
|
-
}, [isProcessing, modalMode, addMessage]);
|
|
3592
|
-
// Handle direct send (Shift+Enter) - interrupts current operation and sends immediately
|
|
3593
|
-
const handleDirectSend = useCallback((msg) => {
|
|
3594
|
-
// Stop current processing
|
|
3595
|
-
setIsProcessing(false);
|
|
3596
|
-
setThinkingState(null);
|
|
3597
|
-
setStreamingResponse('');
|
|
3598
|
-
setEditingQueueIndex(null);
|
|
3599
|
-
// Show what happened
|
|
3600
|
-
addMessage('system', '⚡ Direct send - interrupting current operation');
|
|
3601
|
-
addMessage('user', msg);
|
|
3602
|
-
// Start new agent run with this message
|
|
3603
|
-
setIsProcessing(true);
|
|
3604
|
-
runAgent(msg).finally(() => {
|
|
3605
|
-
setIsProcessing(false);
|
|
3606
|
-
setThinkingState(null);
|
|
3607
|
-
setStreamingResponse('');
|
|
3608
|
-
setEditingQueueIndex(null);
|
|
3609
|
-
});
|
|
3610
|
-
}, [addMessage, runAgent]);
|
|
3611
|
-
// Streaming response component (reused across layouts)
|
|
3612
|
-
const StreamingResponseBox = streamingResponse ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "cyan", children: "\u258C" })] })) : null;
|
|
3613
|
-
// Thinking/Processing indicator component
|
|
3614
|
-
const ProcessingBox = (_jsxs(_Fragment, { children: [isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Waiting for response..." }), isProcessing && streamingResponse && _jsx(StreamingIndicator, { activity: activityState ?? undefined })] }));
|
|
3615
|
-
// Render based on layout
|
|
3616
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [layout === 'split' && (_jsxs(Box, { flexDirection: "row", width: width, children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsx(Text, { color: "yellow", dimColor: true, children: "\u2500 Tools \u2500" }), _jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox] }), _jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderLeft: true, borderColor: "gray", children: [_jsx(Text, { color: "cyan", dimColor: true, children: "\u2500 Response \u2500" }), StreamingResponseBox] })] })), layout === 'classic' && (_jsxs(_Fragment, { children: [_jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox, StreamingResponseBox] })), layout === 'response-top' && (_jsxs(_Fragment, { children: [StreamingResponseBox, _jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox] })), layout === 'response-bottom' && (_jsxs(_Fragment, { children: [_jsx(MessageHistory, { messages: messages, collapseSettings: collapseSettings }), ProcessingBox, StreamingResponseBox] })), debugEnabled && (_jsx(Box, { marginY: 0, children: _jsxs(Text, { dimColor: true, children: ["[dbg] proc=", isProcessing ? 'Y' : 'N', " think=", thinkingState ? 'Y' : 'N', " stream=", streamingResponse.length, " mode=", mode, " queue=", queuedMessages.length, " activity=", activityState?.action || 'none'] }) })), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'sessions' && (_jsx(SessionSelector, { sessions: availableSessions, onSelect: (session) => {
|
|
3617
|
-
// Load history from selected session
|
|
3618
|
-
addMessage('system', `Loading session: ${session.projectName}...`);
|
|
3619
|
-
// Note: We can't easily switch sessions, but we can show the path
|
|
3620
|
-
addMessage('system', `Session path: ${session.projectPath}\nTo load this session, run calliope from that directory.`);
|
|
3621
|
-
setModalMode('none');
|
|
3622
|
-
}, onDelete: (session) => {
|
|
3623
|
-
if (storage.deleteSession(session.id)) {
|
|
3624
|
-
addMessage('system', `🗑️ Deleted session: ${session.projectName}`);
|
|
3625
|
-
// Refresh the list
|
|
3626
|
-
setAvailableSessions(prev => prev.filter(s => s.id !== session.id));
|
|
3627
|
-
}
|
|
3628
|
-
else {
|
|
3629
|
-
addMessage('error', `Failed to delete session: ${session.projectName}`);
|
|
3630
|
-
}
|
|
3631
|
-
}, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), modalMode === 'session-resume' && previousSession && (_jsx(SessionResumePrompt, { session: previousSession, onResume: () => {
|
|
3632
|
-
// Load chat history into context
|
|
3633
|
-
const history = storage.getChatHistory(20);
|
|
3634
|
-
if (history.length > 0) {
|
|
3635
|
-
for (const msg of history) {
|
|
3636
|
-
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
3637
|
-
llmMessages.current.push({
|
|
3638
|
-
role: msg.role,
|
|
3639
|
-
content: msg.content,
|
|
3640
|
-
});
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
3643
|
-
addMessage('system', `✓ Resumed session with ${history.length} messages loaded`);
|
|
3644
|
-
setContextTokens(estimateContextTokens());
|
|
3645
|
-
}
|
|
3646
|
-
setModalMode('none');
|
|
3647
|
-
setPreviousSession(null);
|
|
3648
|
-
}, onNew: () => {
|
|
3649
|
-
addMessage('system', '✓ Starting fresh session');
|
|
3650
|
-
setModalMode('none');
|
|
3651
|
-
setPreviousSession(null);
|
|
3652
|
-
} })), modalMode === 'complexity-warning' && pendingComplexPrompt && (_jsx(ComplexityWarning, { reason: pendingComplexPrompt.complexity.reason || 'Complex operation detected', prompt: typeof pendingComplexPrompt.prompt === 'string' ? pendingComplexPrompt.prompt : undefined, onProceed: async () => {
|
|
3653
|
-
setModalMode('none');
|
|
3654
|
-
const prompt = pendingComplexPrompt.prompt;
|
|
3655
|
-
setPendingComplexPrompt(null);
|
|
3656
|
-
// Proceed with execution
|
|
3657
|
-
saveUndoState();
|
|
3658
|
-
addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
|
|
3659
|
-
setIsProcessing(true);
|
|
3660
|
-
try {
|
|
3661
|
-
await runAgent(prompt);
|
|
3662
|
-
}
|
|
3663
|
-
finally {
|
|
3664
|
-
setIsProcessing(false);
|
|
3665
|
-
}
|
|
3666
|
-
}, onPlan: () => {
|
|
3667
|
-
setModalMode('none');
|
|
3668
|
-
const prompt = pendingComplexPrompt.prompt;
|
|
3669
|
-
setPendingComplexPrompt(null);
|
|
3670
|
-
// Switch to plan mode and proceed
|
|
3671
|
-
setMode('plan');
|
|
3672
|
-
addMessage('system', '📋 Switched to Plan mode - I\'ll describe what I would do without executing.');
|
|
3673
|
-
saveUndoState();
|
|
3674
|
-
addMessage('user', typeof prompt === 'string' ? prompt : JSON.stringify(prompt));
|
|
3675
|
-
setIsProcessing(true);
|
|
3676
|
-
runAgent(prompt).finally(() => setIsProcessing(false));
|
|
3677
|
-
}, onCancel: () => {
|
|
3678
|
-
setModalMode('none');
|
|
3679
|
-
setPendingComplexPrompt(null);
|
|
3680
|
-
addMessage('system', 'Operation cancelled.');
|
|
3681
|
-
} })), modalMode === 'keys' && (_jsx(KeybindingsModal, { onClose: () => setModalMode('none') })), _jsx(ChatInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: handleEscape, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, editingQueueIndex: editingQueueIndex, onQueueMessage: (msg) => {
|
|
3682
|
-
setQueuedMessages(prev => [...prev, msg]);
|
|
3683
|
-
addMessage('system', `📨 Queued: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}"`);
|
|
3684
|
-
}, onEditQueuedMessage: handleEditQueuedMessage, onSetEditingQueueIndex: setEditingQueueIndex, onDirectSend: handleDirectSend, cwd: process.cwd(), suggestions: suggestions, onSuggestionsChange: setSuggestions, onNavigateHistory: navigateHistory,
|
|
3685
|
-
// Smart suggestions context
|
|
3686
|
-
currentMode: mode, contextPercentage: Math.round((contextTokens / getModelContextLimit(actualProvider, actualModel)) * 100), recentCommands: recentCommands, hasGitRepo: hasGitRepo }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
|
|
3687
|
-
}
|
|
3688
|
-
// ============================================================================
|
|
3689
|
-
// App Wrapper & Entry Point
|
|
3690
|
-
// ============================================================================
|
|
3691
|
-
function App() {
|
|
3692
|
-
const [resetKey, setResetKey] = React.useState(0);
|
|
3693
|
-
const handleReset = React.useCallback(() => {
|
|
3694
|
-
setResetKey(k => k + 1);
|
|
3695
|
-
}, []);
|
|
3696
|
-
return (_jsx(ErrorBoundary, { onReset: handleReset, children: _jsx(TerminalChat, {}, resetKey) }));
|
|
3697
|
-
}
|
|
3698
|
-
// Print banner before Ink takes over (stays fixed at top)
|
|
3699
|
-
function printBanner() {
|
|
3700
|
-
const provider = selectProvider(config.get('defaultProvider'));
|
|
3701
|
-
const model = config.get('defaultModel') || DEFAULT_MODELS[provider];
|
|
3702
|
-
const cyan = '\x1b[36m';
|
|
3703
|
-
const cyanBright = '\x1b[96m';
|
|
3704
|
-
const dim = '\x1b[2m';
|
|
3705
|
-
const reset = '\x1b[0m';
|
|
3706
|
-
console.log();
|
|
3707
|
-
console.log(`${cyanBright}${BANNER_LINES[0]}${reset}`);
|
|
3708
|
-
console.log(`${cyanBright}${BANNER_LINES[1]}${reset}`);
|
|
3709
|
-
console.log(`${cyan}${BANNER_LINES[2]}${reset}`);
|
|
3710
|
-
console.log(`${cyan}${BANNER_LINES[3]}${reset}`);
|
|
3711
|
-
console.log(`${cyanBright}${BANNER_LINES[4]}${reset}`);
|
|
3712
|
-
console.log(`${cyan}${BANNER_LINES[5]}${reset}`);
|
|
3713
|
-
console.log();
|
|
3714
|
-
console.log(`${dim} The Muse of Digital Eloquence${reset}`);
|
|
3715
|
-
console.log();
|
|
3716
|
-
console.log(`${dim} v${getVersion()} | ${provider}:${model}${reset}`);
|
|
3717
|
-
console.log(`${dim} /help for commands | ESC to exit${reset}`);
|
|
3718
|
-
console.log();
|
|
3719
|
-
}
|
|
3720
|
-
export async function startInkCLI(options = {}) {
|
|
3721
|
-
// Set module-level agterm state
|
|
3722
|
-
moduleAgtermEnabled = options.agtermEnabled ?? false;
|
|
3723
|
-
// Print banner BEFORE Ink starts - it stays fixed at the top
|
|
3724
|
-
printBanner();
|
|
3725
|
-
const { waitUntilExit } = render(_jsx(App, {}), {
|
|
3726
|
-
patchConsole: true, // Prevent console.log during session from mixing with Ink
|
|
3727
|
-
});
|
|
3728
|
-
await waitUntilExit();
|
|
3729
|
-
}
|
|
3730
|
-
//# sourceMappingURL=ui-cli.js.map
|