@wangzhizhi/remi 0.1.224 → 0.1.244
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/README.md +206 -2
- package/dist/help.js +1 -1
- package/dist/initPrompt.js +1 -1
- package/dist/setup.js +5 -4
- package/dist/statusCard.js +1 -1
- package/dist/statusline.js +1 -1
- package/dist/tui/RemiApp.js +55 -7
- package/dist/tui/hooksPanel.js +2 -2
- package/dist/tui/renderers/MessageList.js +10 -7
- package/dist/version.js +1 -1
- package/node_modules/@remi/compact/dist/index.js +13 -5
- package/node_modules/@remi/compact/package.json +1 -1
- package/node_modules/@remi/config/dist/index.js +816 -38
- package/node_modules/@remi/config/package.json +1 -1
- package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
- package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
- package/node_modules/@remi/core/dist/index.js +630 -33
- package/node_modules/@remi/core/dist/stableHash.js +30 -0
- package/node_modules/@remi/core/package.json +1 -1
- package/node_modules/@remi/hooks/package.json +1 -1
- package/node_modules/@remi/llm/package.json +1 -1
- package/node_modules/@remi/mcp/dist/index.js +657 -0
- package/node_modules/@remi/mcp/package.json +8 -0
- package/node_modules/@remi/memory/package.json +1 -1
- package/node_modules/@remi/permissions/package.json +1 -1
- package/node_modules/@remi/sessions/dist/index.js +38 -0
- package/node_modules/@remi/sessions/package.json +1 -1
- package/node_modules/@remi/skills/dist/index.js +2 -2
- package/node_modules/@remi/skills/package.json +1 -1
- package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
- package/node_modules/@remi/terminal-markdown/package.json +1 -1
- package/node_modules/@remi/tools/dist/index.js +1 -1
- package/node_modules/@remi/tools/package.json +1 -1
- package/package.json +14 -12
- package/prompt/tool-use-system.md +19 -3
package/README.md
CHANGED
|
@@ -1,9 +1,213 @@
|
|
|
1
1
|
# Remi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Remi is an open-source Coding Agent CLI.
|
|
6
|
+
|
|
7
|
+
## 1. Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @wangzhizhi/remi
|
|
11
|
+
remi
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 2. Build From Source
|
|
15
|
+
|
|
16
|
+
### 2.1 Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 24 LTS
|
|
19
|
+
- pnpm 11.4.0 through Corepack
|
|
20
|
+
|
|
21
|
+
### 2.2 Build Commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
corepack enable
|
|
25
|
+
pnpm install --frozen-lockfile
|
|
26
|
+
pnpm build
|
|
27
|
+
pnpm agent --help
|
|
28
|
+
pnpm agent
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2.3 Useful Checks
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm typecheck
|
|
35
|
+
pnpm pack:npm
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 3. Model Configuration
|
|
39
|
+
|
|
40
|
+
Configure a model before using Remi for agent workflows. You can run `remi setup` or create the config file manually. The default preset references API keys through environment variables, does not store raw keys, and includes the reference filesystem MCP server.
|
|
41
|
+
|
|
42
|
+
### 3.1 macOS / Linux
|
|
43
|
+
|
|
44
|
+
Config path: `~/.remi/config.toml`.
|
|
45
|
+
|
|
46
|
+
Create it with `remi setup`, or create `~/.remi/config.toml` manually.
|
|
4
47
|
|
|
5
48
|
```bash
|
|
6
|
-
npm install -g @wangzhizhi/remi@0.1.224
|
|
7
49
|
remi setup
|
|
50
|
+
export DEEPSEEK_API_KEY="<your-key>"
|
|
8
51
|
remi
|
|
9
52
|
```
|
|
53
|
+
|
|
54
|
+
### 3.2 Windows
|
|
55
|
+
|
|
56
|
+
Config path: `%USERPROFILE%\.remi\config.toml`.
|
|
57
|
+
|
|
58
|
+
Create it with `remi setup`, or create `%USERPROFILE%\.remi\config.toml` manually. Set `DEEPSEEK_API_KEY` in the user environment and restart the terminal.
|
|
59
|
+
|
|
60
|
+
### 3.3 Commands
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
remi model list
|
|
64
|
+
remi model providers
|
|
65
|
+
remi model profiles
|
|
66
|
+
remi model doctor
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3.4 Model Profiles
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
active_profile = "deepseek-v4-flash"
|
|
73
|
+
|
|
74
|
+
[[profiles]]
|
|
75
|
+
provider = "openai-compatible"
|
|
76
|
+
model_id = "deepseek-v4-flash"
|
|
77
|
+
display_name = "DeepSeek-V4-Flash"
|
|
78
|
+
base_url = "https://api.deepseek.com"
|
|
79
|
+
api_key = "${DEEPSEEK_API_KEY}"
|
|
80
|
+
context_window = 1000000
|
|
81
|
+
|
|
82
|
+
[[profiles]]
|
|
83
|
+
provider = "openai-compatible"
|
|
84
|
+
model_id = "deepseek-v4-pro"
|
|
85
|
+
display_name = "DeepSeek-V4-Pro"
|
|
86
|
+
base_url = "https://api.deepseek.com"
|
|
87
|
+
api_key = "${DEEPSEEK_API_KEY}"
|
|
88
|
+
context_window = 1000000
|
|
89
|
+
|
|
90
|
+
[[profiles]]
|
|
91
|
+
provider = "openai-compatible"
|
|
92
|
+
model_id = "mimo-v2.5-pro"
|
|
93
|
+
display_name = "MiMo-V2.5-Pro"
|
|
94
|
+
base_url = "https://api.xiaomimimo.com/v1"
|
|
95
|
+
api_key = "${MIMO_API_KEY}"
|
|
96
|
+
context_window = 1000000
|
|
97
|
+
|
|
98
|
+
[[profiles]]
|
|
99
|
+
provider = "openai-compatible"
|
|
100
|
+
model_id = "doubao-seed-2.0-pro"
|
|
101
|
+
display_name = "Doubao-Seed-2.0"
|
|
102
|
+
base_url = "https://ark.cn-beijing.volces.com/api/v3"
|
|
103
|
+
api_key = "${ARK_API_KEY}"
|
|
104
|
+
context_window = 200000
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Each `[[profiles]]` entry is a complete model profile. Add or remove profiles as needed, then set `active_profile` to the one Remi should use by default. Remi uses `model_id` as both the profile name and the provider model id. Remi maps all agent roles to that profile's model unless a project-level setting overrides it.
|
|
108
|
+
|
|
109
|
+
Tool use, reasoning support, output token reserve, and reasoning effort use Remi defaults. The default effort level is `max`.
|
|
110
|
+
|
|
111
|
+
Useful fields:
|
|
112
|
+
|
|
113
|
+
- `provider`
|
|
114
|
+
- `model_id`
|
|
115
|
+
- `display_name`
|
|
116
|
+
- `base_url`
|
|
117
|
+
- `api_key`
|
|
118
|
+
- `context_window`
|
|
119
|
+
|
|
120
|
+
### 3.5 Remi Home
|
|
121
|
+
|
|
122
|
+
`REMI_HOME` can be used to point Remi at a different home directory.
|
|
123
|
+
|
|
124
|
+
## 4. Hooks
|
|
125
|
+
|
|
126
|
+
### 4.1 Configuration
|
|
127
|
+
|
|
128
|
+
Hooks are configured in `~/.remi/config.toml` or in the project-specific config under `~/.remi/projects/<project-id>/config.toml`. Open `/hooks` inside the TUI to inspect loaded hooks.
|
|
129
|
+
|
|
130
|
+
### 4.2 Blocking Behavior
|
|
131
|
+
|
|
132
|
+
Hook commands receive a JSON payload on stdin. Exit code `2` blocks the current action. A hook may also print JSON such as `{ "continue": false, "reason": "..." }`.
|
|
133
|
+
|
|
134
|
+
### 4.3 Example
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
[[hooks]]
|
|
138
|
+
event = "pre_tool_use"
|
|
139
|
+
matcher = "*"
|
|
140
|
+
command = "node ./scripts/audit-tool.js"
|
|
141
|
+
timeout = 10
|
|
142
|
+
status_message = "Auditing tool call"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Task completion popup:
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
# macOS
|
|
149
|
+
[[hooks]]
|
|
150
|
+
event = "stop"
|
|
151
|
+
command = "osascript -e 'display notification \"Task completed\" with title \"Remi\"'"
|
|
152
|
+
timeout = 5
|
|
153
|
+
status_message = "Showing completion notification"
|
|
154
|
+
|
|
155
|
+
# Windows
|
|
156
|
+
[[hooks]]
|
|
157
|
+
event = "stop"
|
|
158
|
+
command = "powershell -NoProfile -Command \"Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Task completed', 'Remi')\""
|
|
159
|
+
timeout = 5
|
|
160
|
+
status_message = "Showing completion notification"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 5. MCP
|
|
164
|
+
|
|
165
|
+
### 5.1 Supported Transport
|
|
166
|
+
|
|
167
|
+
Remi supports stdio MCP servers configured in `~/.remi/config.toml`. Discovered MCP tools are bridged into Remi's tool and permission system.
|
|
168
|
+
|
|
169
|
+
`remi setup` writes a default filesystem MCP server for the current working directory. Edit the section to change allowed paths, disable it, or add more servers.
|
|
170
|
+
|
|
171
|
+
HTTP/SSE MCP servers, MCP resources, MCP prompts, crash recovery, and a dedicated MCP TUI panel are not exposed yet.
|
|
172
|
+
|
|
173
|
+
### 5.2 Example
|
|
174
|
+
|
|
175
|
+
```toml
|
|
176
|
+
[mcp.servers.filesystem]
|
|
177
|
+
type = "stdio"
|
|
178
|
+
command = "npx"
|
|
179
|
+
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
180
|
+
timeout_ms = 15000
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Windows uses a `cmd` wrapper for `npx`:
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
[mcp.servers.filesystem]
|
|
187
|
+
type = "stdio"
|
|
188
|
+
command = "cmd"
|
|
189
|
+
args = ["/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
190
|
+
timeout_ms = 15000
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Set `enabled = false` in the server section to keep the entry but disable it.
|
|
194
|
+
|
|
195
|
+
## 6. Skills
|
|
196
|
+
|
|
197
|
+
Remi discovers skills from:
|
|
198
|
+
|
|
199
|
+
- `skills/<name>/SKILL.md` in the current project tree
|
|
200
|
+
- `~/.remi/skills/<name>/SKILL.md`
|
|
201
|
+
- `~/.agents/skills/<name>/SKILL.md`
|
|
202
|
+
|
|
203
|
+
## 7. Contributing
|
|
204
|
+
|
|
205
|
+
External contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). Keep changes narrow, compile before submitting, and avoid adding private notes or credentials.
|
|
206
|
+
|
|
207
|
+
## 8. Security
|
|
208
|
+
|
|
209
|
+
Please do not open public issues for vulnerabilities. See [SECURITY.md](SECURITY.md) for private reporting guidance.
|
|
210
|
+
|
|
211
|
+
## 9. License
|
|
212
|
+
|
|
213
|
+
MIT. See [LICENSE](LICENSE).
|
package/dist/help.js
CHANGED
|
@@ -9,7 +9,7 @@ export function formatHelp() {
|
|
|
9
9
|
' remi --plain Start readline fallback',
|
|
10
10
|
' remi --tui Force TUI mode',
|
|
11
11
|
' remi doctor Check local environment',
|
|
12
|
-
' remi setup Create user config in ~/.remi/config.
|
|
12
|
+
' remi setup Create user config in ~/.remi/config.toml',
|
|
13
13
|
' remi model list Show configured model aliases',
|
|
14
14
|
' remi model doctor Check model configuration',
|
|
15
15
|
' remi --help Show help',
|
package/dist/initPrompt.js
CHANGED
|
@@ -5,7 +5,7 @@ Treat this slash-command task as standalone for the current Remi runtime cwd. Do
|
|
|
5
5
|
|
|
6
6
|
Work in this order:
|
|
7
7
|
|
|
8
|
-
1. Explore the repository before writing. Read
|
|
8
|
+
1. Explore the repository before writing. Read the README, package/workspace manifests, build or CI config, and existing project-instruction files. Check Remi instruction names first (AGENTS.md, AGENT.md, REMI.md), then look for common editor or assistant rule files in places such as .cursor/, .github/, and top-level rule files.
|
|
9
9
|
2. Identify only non-obvious guidance: build/test/lint commands, package manager/runtime requirements, architecture boundaries, repo workflow, local setup gotchas, testing quirks, generated files, security rules, or user/team preferences already documented in the repo.
|
|
10
10
|
3. If AGENTS.md already exists, read it first and update it narrowly. Do not silently overwrite existing guidance.
|
|
11
11
|
4. Create or update AGENTS.md at the project root with a short, practical structure. Start it with:
|
package/dist/setup.js
CHANGED
|
@@ -11,12 +11,12 @@ export function runSetupCommand(args = [], options = {}) {
|
|
|
11
11
|
if (unknown) {
|
|
12
12
|
return { code: 1, output: `Unknown setup option: ${unknown}\n\n${formatSetupHelp()}` };
|
|
13
13
|
}
|
|
14
|
-
const homeDir = options.homeDir ?? homedir();
|
|
14
|
+
const homeDir = options.homeDir ?? process.env['REMI_HOME'] ?? homedir();
|
|
15
15
|
const platform = options.platform ?? process.platform;
|
|
16
16
|
const path = userConfigPath(homeDir);
|
|
17
17
|
const existed = existsSync(path);
|
|
18
18
|
if (!existed || force) {
|
|
19
|
-
writeUserRemiConfig(createDeepSeekPresetConfig(), homeDir);
|
|
19
|
+
writeUserRemiConfig(createDeepSeekPresetConfig({ platform }), homeDir);
|
|
20
20
|
}
|
|
21
21
|
return {
|
|
22
22
|
code: 0,
|
|
@@ -26,10 +26,11 @@ export function runSetupCommand(args = [], options = {}) {
|
|
|
26
26
|
export function formatSetupHelp() {
|
|
27
27
|
return [
|
|
28
28
|
'Usage:',
|
|
29
|
-
' remi setup Create ~/.remi/config.
|
|
30
|
-
' remi setup --force Rewrite ~/.remi/config.
|
|
29
|
+
' remi setup Create ~/.remi/config.toml if it does not exist',
|
|
30
|
+
' remi setup --force Rewrite ~/.remi/config.toml with the default preset',
|
|
31
31
|
'',
|
|
32
32
|
'The generated config references DEEPSEEK_API_KEY by environment variable and never stores a raw key.',
|
|
33
|
+
'It also enables the reference filesystem MCP server for the current working directory; edit config.toml to change or disable it.',
|
|
33
34
|
].join('\n');
|
|
34
35
|
}
|
|
35
36
|
function formatSetupResult({ path, wrote, platform }) {
|
package/dist/statusCard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join, parse, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { defaultResponseStyleId, responseStylePresets } from '@remi/core';
|
|
4
|
+
import { defaultResponseStyleId, responseStylePresets, } from '@remi/core';
|
|
5
5
|
import { loadRemiConfig, normalizePermissionProfile } from '@remi/config';
|
|
6
6
|
import { createModelRouter } from '@remi/llm';
|
|
7
7
|
import { loadGitStatus } from './git.js';
|
package/dist/statusline.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadRemiConfig, readProjectRemiConfig, statusLineItemIds, writeProjectRemiConfig } from '@remi/config';
|
|
2
2
|
import { t } from './i18n.js';
|
|
3
|
-
export const defaultStatusLineItems = ['model', 'context-remaining', 'cwd'];
|
|
3
|
+
export const defaultStatusLineItems = ['model', 'context-remaining', 'total-input-tokens', 'cache-hit-rate', 'cwd'];
|
|
4
4
|
export const statusLineCatalog = [
|
|
5
5
|
{ id: 'model', label: 'model', description: 'Current main model' },
|
|
6
6
|
{ id: 'cwd', label: 'cwd', description: 'Current working directory' },
|
package/dist/tui/RemiApp.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { Box, Text, useApp, useInput, usePaste, useWindowSize } from 'ink';
|
|
6
6
|
import { estimateSessionContextTrace, runChatTurn } from '@remi/core';
|
|
7
|
-
import {
|
|
7
|
+
import { compactCurrentSession } from '@remi/core';
|
|
8
8
|
import { loadRemiConfig, normalizePermissionProfile } from '@remi/config';
|
|
9
9
|
import { createModelRouter } from '@remi/llm';
|
|
10
10
|
import { loadSkillIndex } from '@remi/skills';
|
|
@@ -525,7 +525,13 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
525
525
|
setProgressLabelOverride(t(language, 'compact.progress'));
|
|
526
526
|
try {
|
|
527
527
|
await new Promise(resolve => setTimeout(resolve, compactProgressDelayMs));
|
|
528
|
-
const compactResult =
|
|
528
|
+
const compactResult = await compactCurrentSession({
|
|
529
|
+
cwd,
|
|
530
|
+
sessionId,
|
|
531
|
+
trigger: 'manual',
|
|
532
|
+
responseStyle,
|
|
533
|
+
...(mainModelAlias ? { modelOverrides: { main: mainModelAlias } } : {}),
|
|
534
|
+
});
|
|
529
535
|
appendMessage({
|
|
530
536
|
kind: 'compact-boundary',
|
|
531
537
|
summary: t(language, 'compact.done', {
|
|
@@ -560,6 +566,7 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
560
566
|
const abortController = new AbortController();
|
|
561
567
|
activeAbortController.current = abortController;
|
|
562
568
|
let pendingAssistantText = '';
|
|
569
|
+
let pendingAssistantTextDraft = false;
|
|
563
570
|
let assistantFlushTimer;
|
|
564
571
|
let lastAssistantFlushAt = 0;
|
|
565
572
|
const flushAssistantText = () => {
|
|
@@ -571,9 +578,11 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
571
578
|
return;
|
|
572
579
|
}
|
|
573
580
|
const text = pendingAssistantText;
|
|
581
|
+
const draft = pendingAssistantTextDraft;
|
|
574
582
|
pendingAssistantText = '';
|
|
583
|
+
pendingAssistantTextDraft = false;
|
|
575
584
|
lastAssistantFlushAt = Date.now();
|
|
576
|
-
setMessages(current => appendAssistantDelta(current, text));
|
|
585
|
+
setMessages(current => appendAssistantDelta(current, text, draft));
|
|
577
586
|
};
|
|
578
587
|
const scheduleAssistantFlush = (delayMs = assistantStreamFlushIntervalMs) => {
|
|
579
588
|
if (assistantFlushTimer) {
|
|
@@ -584,8 +593,12 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
584
593
|
flushAssistantText();
|
|
585
594
|
}, delayMs);
|
|
586
595
|
};
|
|
587
|
-
const queueAssistantText = (text) => {
|
|
596
|
+
const queueAssistantText = (text, draft = false) => {
|
|
597
|
+
if (pendingAssistantText.length > 0 && pendingAssistantTextDraft !== draft) {
|
|
598
|
+
flushAssistantText();
|
|
599
|
+
}
|
|
588
600
|
pendingAssistantText += text;
|
|
601
|
+
pendingAssistantTextDraft = draft;
|
|
589
602
|
const now = Date.now();
|
|
590
603
|
const elapsedSinceFlush = lastAssistantFlushAt === 0 ? Infinity : now - lastAssistantFlushAt;
|
|
591
604
|
if (lastAssistantFlushAt === 0 ||
|
|
@@ -609,6 +622,7 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
609
622
|
askUserQuestion: requestAskUserQuestion,
|
|
610
623
|
permissionRules: () => sessionPermissionRules.current,
|
|
611
624
|
signal: abortController.signal,
|
|
625
|
+
previewBufferedDeltas: true,
|
|
612
626
|
...(mainModelAlias ? { modelOverrides: { main: mainModelAlias } } : {}),
|
|
613
627
|
};
|
|
614
628
|
for await (const event of chatRunner(chatInput, chatConfig)) {
|
|
@@ -658,6 +672,19 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
658
672
|
setWorkingStartedAt(undefined);
|
|
659
673
|
queueAssistantText(event.text);
|
|
660
674
|
}
|
|
675
|
+
else if (event.type === 'draft_delta') {
|
|
676
|
+
setWorkingStartedAt(undefined);
|
|
677
|
+
queueAssistantText(event.text, true);
|
|
678
|
+
}
|
|
679
|
+
else if (event.type === 'draft_reset') {
|
|
680
|
+
flushAssistantText();
|
|
681
|
+
setMessages(current => removeAssistantDraft(current));
|
|
682
|
+
setWorkingStartedAt(Date.now());
|
|
683
|
+
}
|
|
684
|
+
else if (event.type === 'draft_commit') {
|
|
685
|
+
flushAssistantText();
|
|
686
|
+
setMessages(current => commitAssistantDraft(current));
|
|
687
|
+
}
|
|
661
688
|
else if (event.type === 'tool_call') {
|
|
662
689
|
setWorkingStartedAt(undefined);
|
|
663
690
|
flushAssistantText();
|
|
@@ -674,6 +701,11 @@ export function RemiApp({ cwd = process.cwd(), chatRunner = runChatTurn, initial
|
|
|
674
701
|
setWorkingStartedAt(nextWorkingStartedAt);
|
|
675
702
|
continue;
|
|
676
703
|
}
|
|
704
|
+
if (event.toolName === 'ask_user_question') {
|
|
705
|
+
setMessages(current => removeMatchingToolCall(finalizeStreamingMessages(current), event.callId));
|
|
706
|
+
setWorkingStartedAt(nextWorkingStartedAt);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
677
709
|
setMessages(current => replaceToolCallWithResult(finalizeStreamingMessages(current), {
|
|
678
710
|
kind: 'tool-result',
|
|
679
711
|
callId: event.callId,
|
|
@@ -1967,7 +1999,7 @@ function findActiveBottomPanelIndex(messages) {
|
|
|
1967
1999
|
}
|
|
1968
2000
|
return -1;
|
|
1969
2001
|
}
|
|
1970
|
-
function appendAssistantDelta(messages, text) {
|
|
2002
|
+
function appendAssistantDelta(messages, text, draft = false) {
|
|
1971
2003
|
const next = [...messages];
|
|
1972
2004
|
const index = next.length - 1;
|
|
1973
2005
|
const currentMessage = next[index];
|
|
@@ -1976,14 +2008,30 @@ function appendAssistantDelta(messages, text) {
|
|
|
1976
2008
|
...currentMessage,
|
|
1977
2009
|
text: currentMessage.text + text,
|
|
1978
2010
|
streaming: true,
|
|
2011
|
+
draft: currentMessage.draft || draft,
|
|
1979
2012
|
};
|
|
1980
2013
|
return next;
|
|
1981
2014
|
}
|
|
1982
|
-
return [...next, { kind: 'assistant', text, streaming: true }];
|
|
2015
|
+
return [...next, { kind: 'assistant', text, streaming: true, ...(draft ? { draft: true } : {}) }];
|
|
2016
|
+
}
|
|
2017
|
+
function removeAssistantDraft(messages) {
|
|
2018
|
+
return messages.filter(message => !(message.kind === 'assistant' && message.draft));
|
|
2019
|
+
}
|
|
2020
|
+
function commitAssistantDraft(messages) {
|
|
2021
|
+
return messages.map(message => {
|
|
2022
|
+
if (message.kind !== 'assistant' || !message.draft) {
|
|
2023
|
+
return message;
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
kind: 'assistant',
|
|
2027
|
+
text: message.text,
|
|
2028
|
+
...(message.streaming !== undefined ? { streaming: message.streaming } : {}),
|
|
2029
|
+
};
|
|
2030
|
+
});
|
|
1983
2031
|
}
|
|
1984
2032
|
function finalizeStreamingMessages(messages) {
|
|
1985
2033
|
return messages
|
|
1986
|
-
.filter(message => !(message.kind === 'assistant' && message.text.length === 0))
|
|
2034
|
+
.filter(message => !(message.kind === 'assistant' && (message.text.length === 0 || message.draft)))
|
|
1987
2035
|
.map(message => (message.kind === 'assistant' ? { ...message, streaming: false } : message));
|
|
1988
2036
|
}
|
|
1989
2037
|
function removeMatchingToolCall(messages, callId) {
|
package/dist/tui/hooksPanel.js
CHANGED
|
@@ -15,8 +15,8 @@ export function createHooksPanelMessage(cwd, language) {
|
|
|
15
15
|
? '来自配置的 lifecycle hooks。'
|
|
16
16
|
: 'Lifecycle hooks from config and enabled plugins.',
|
|
17
17
|
emptyText: language === 'zh-Hans'
|
|
18
|
-
? '未配置 hooks。编辑 config.
|
|
19
|
-
: 'No hooks configured. Add hooks in config.
|
|
18
|
+
? '未配置 hooks。编辑 config.toml 后重新打开 /hooks。'
|
|
19
|
+
: 'No hooks configured. Add hooks in config.toml, then reopen /hooks.',
|
|
20
20
|
detailEmptyText: language === 'zh-Hans'
|
|
21
21
|
? '这个事件没有安装 hooks。'
|
|
22
22
|
: 'No hooks installed for this event.',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useMemo } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { ShellInlineCommand, StreamingMarkdown, StructuredDiffView, TerminalInlineMarkdown, TerminalMarkdown, applyTerminalSyntaxTheme, displayWidth, } from '@remi/terminal-markdown';
|
|
4
|
+
import { ShellInlineCommand, StreamingMarkdown, StructuredDiffView, TerminalInlineMarkdown, TerminalMarkdown, applyTerminalSyntaxTheme, compactInlineUrlsForDisplay, displayWidth, } from '@remi/terminal-markdown';
|
|
5
5
|
import { accentColorValue, remiDarkTheme } from '../theme.js';
|
|
6
6
|
import { ActivityGlyph, ActivityText, AnimatedDots, FlowingText, activityBaseColor, useActivityTick, WorkingIndicator } from './WorkingIndicator.js';
|
|
7
7
|
import { formatPermissionFileActionFromParts } from '../../permissionDisplay.js';
|
|
@@ -165,7 +165,8 @@ function MessageGroup({ messages, startIndex, width, language, slashCommandHints
|
|
|
165
165
|
}
|
|
166
166
|
function UserMessage({ text, marginTop, slashCommandHints, }) {
|
|
167
167
|
const slashCommand = executableSlashCommandPrefix(text, slashCommandHints);
|
|
168
|
-
|
|
168
|
+
const displayText = compactInlineUrlsForDisplay(text);
|
|
169
|
+
return (_jsxs(Box, { marginTop: marginTop, paddingX: 1, paddingY: 1, backgroundColor: remiDarkTheme.composerBackground, children: [_jsx(Text, { color: remiDarkTheme.muted, children: '> ' }), slashCommand ? (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: remiDarkTheme.accent, children: slashCommand.command }), _jsx(Text, { color: remiDarkTheme.text, children: compactInlineUrlsForDisplay(slashCommand.rest) })] })) : (_jsx(Text, { color: remiDarkTheme.text, wrap: "wrap", children: displayText }))] }));
|
|
169
170
|
}
|
|
170
171
|
function AssistantMessage({ text, streaming, marginTop, width, language, }) {
|
|
171
172
|
const markdownTheme = useMarkdownTheme();
|
|
@@ -1290,14 +1291,14 @@ function AskUserQuestionPanel({ message, marginTop, width, }) {
|
|
|
1290
1291
|
const descriptionColor = isSelected ? remiDarkTheme.accent : remiDarkTheme.muted;
|
|
1291
1292
|
const label = truncateTextToWidth(option.label, optionLayout.labelWidth);
|
|
1292
1293
|
const descriptionLines = option.description ? wrapTextLines(option.description, optionLayout.descriptionWidth, 2) : [];
|
|
1293
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth, children: _jsx(Text, { color: labelColor, children: isSelected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: optionLayout.labelWidth, children: _jsx(Text, { color: labelColor, children: label }) }), descriptionLines[0] ? (_jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, children: descriptionLines[0] }) })) : null] }), descriptionLines.slice(1).map((line, lineIndex) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth + optionLayout.labelWidth, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, children: line }) })] }, `${option.id ?? index}-description-${lineIndex}`)))] }, `${option.id ?? index}-${option.label}`));
|
|
1294
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth, children: _jsx(Text, { color: labelColor, children: isSelected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: optionLayout.labelWidth, children: _jsx(Text, { color: labelColor, children: label }) }), _jsx(Box, { width: optionLayout.gapWidth }), descriptionLines[0] ? (_jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, children: descriptionLines[0] }) })) : null] }), descriptionLines.slice(1).map((line, lineIndex) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth + optionLayout.labelWidth + optionLayout.gapWidth, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, children: line }) })] }, `${option.id ?? index}-description-${lineIndex}`)))] }, `${option.id ?? index}-${option.label}`));
|
|
1294
1295
|
}), _jsx(AskUserQuestionCustomRow, { message: message, index: message.options.length, selected: selectedIndex === message.options.length, layout: optionLayout }), _jsx(Text, { color: remiDarkTheme.muted, children: message.helpText })] }));
|
|
1295
1296
|
}
|
|
1296
1297
|
function AskUserQuestionCustomRow({ message, index, selected, layout, }) {
|
|
1297
1298
|
const labelColor = selected ? remiDarkTheme.accent : remiDarkTheme.text;
|
|
1298
1299
|
const customText = message.customText.trimEnd();
|
|
1299
1300
|
const cursor = selected ? '█' : '';
|
|
1300
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: layout.markerWidth, children: _jsx(Text, { color: labelColor, children: selected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: layout.labelWidth, children: _jsx(Text, { color: labelColor, children: truncateTextToWidth(message.customLabel, layout.labelWidth) }) }), _jsx(Box, { width: layout.descriptionWidth, children: _jsx(Text, { color: labelColor, children: truncateTextToWidth(`${customText}${cursor}`, layout.descriptionWidth) }) })] }));
|
|
1301
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: layout.markerWidth, children: _jsx(Text, { color: labelColor, children: selected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: layout.labelWidth, children: _jsx(Text, { color: labelColor, children: truncateTextToWidth(message.customLabel, layout.labelWidth) }) }), _jsx(Box, { width: layout.gapWidth }), _jsx(Box, { width: layout.descriptionWidth, children: _jsx(Text, { color: labelColor, children: truncateTextToWidth(`${customText}${cursor}`, layout.descriptionWidth) }) })] }));
|
|
1301
1302
|
}
|
|
1302
1303
|
function PermissionRequestCard({ message, marginTop, width, }) {
|
|
1303
1304
|
const selectedIndex = normalizePanelSelectedIndex(message.selectedIndex, message.options.length);
|
|
@@ -1310,7 +1311,7 @@ function PermissionRequestCard({ message, marginTop, width, }) {
|
|
|
1310
1311
|
const descriptionColor = isSelected ? remiDarkTheme.accent : remiDarkTheme.muted;
|
|
1311
1312
|
const label = truncateTextToWidth(option.label, optionLayout.labelWidth);
|
|
1312
1313
|
const descriptionLines = option.description ? wrapTextLines(option.description, optionLayout.descriptionWidth, 3) : [];
|
|
1313
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth, children: _jsx(Text, { color: labelColor, children: isSelected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: optionLayout.labelWidth, children: _jsx(Text, { color: labelColor, bold: isSelected, children: label }) }), descriptionLines[0] ? (_jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, bold: isSelected, children: descriptionLines[0] }) })) : null] }), descriptionLines.slice(1).map((line, lineIndex) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth + optionLayout.labelWidth, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, bold: isSelected, children: line }) })] }, `${option.id}-description-${lineIndex}`)))] }, option.id));
|
|
1314
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth, children: _jsx(Text, { color: labelColor, children: isSelected ? '>' : `${index + 1}.` }) }), _jsx(Box, { width: optionLayout.labelWidth, children: _jsx(Text, { color: labelColor, bold: isSelected, children: label }) }), _jsx(Box, { width: optionLayout.gapWidth }), descriptionLines[0] ? (_jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, bold: isSelected, children: descriptionLines[0] }) })) : null] }), descriptionLines.slice(1).map((line, lineIndex) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: optionLayout.markerWidth + optionLayout.labelWidth + optionLayout.gapWidth, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: optionLayout.descriptionWidth, children: _jsx(Text, { color: descriptionColor, bold: isSelected, children: line }) })] }, `${option.id}-description-${lineIndex}`)))] }, option.id));
|
|
1314
1315
|
}), _jsx(Text, { color: remiDarkTheme.muted, children: message.helpText })] }));
|
|
1315
1316
|
}
|
|
1316
1317
|
function PermissionRequestActionLine({ message, width }) {
|
|
@@ -1337,8 +1338,10 @@ function permissionPanelInnerWidth(width) {
|
|
|
1337
1338
|
function permissionOptionLayout(width) {
|
|
1338
1339
|
const markerWidth = 4;
|
|
1339
1340
|
const labelWidth = Math.min(24, Math.max(16, Math.floor(width * 0.28)));
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1341
|
+
const gapWidth = 1;
|
|
1342
|
+
const availableDescriptionWidth = Math.max(12, width - markerWidth - labelWidth - gapWidth);
|
|
1343
|
+
const descriptionWidth = Math.min(80, availableDescriptionWidth);
|
|
1344
|
+
return { markerWidth, labelWidth, gapWidth, descriptionWidth };
|
|
1342
1345
|
}
|
|
1343
1346
|
function wrapTextLines(value, width, maxLines) {
|
|
1344
1347
|
const text = value.replace(/\s*\r?\n\s*/g, ' ').trim();
|
package/dist/version.js
CHANGED
|
@@ -186,23 +186,31 @@ export function buildCompactSummary(events, options = {}) {
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
export function compactSession(cwd, sessionId, options = {}) {
|
|
189
|
-
const sessionMemory = options.sessionMemorySummary ??
|
|
189
|
+
const sessionMemory = options.sessionMemorySummary ??
|
|
190
|
+
(options.updateSessionMemory === false ? readSessionMemory(cwd, sessionId)?.content : freshSessionMemoryForCompact(cwd, sessionId, options));
|
|
190
191
|
const result = buildCompactSummary(readSessionEvents(cwd, sessionId), {
|
|
191
192
|
...options,
|
|
192
193
|
...(sessionMemory ? { sessionMemorySummary: sessionMemory } : {}),
|
|
193
194
|
});
|
|
195
|
+
const summaryOverride = options.summaryOverride?.trim();
|
|
196
|
+
const compactResult = summaryOverride
|
|
197
|
+
? {
|
|
198
|
+
...result,
|
|
199
|
+
summary: truncateText(summaryOverride, options.maxSummaryChars ?? defaultMaxSummaryChars),
|
|
200
|
+
}
|
|
201
|
+
: result;
|
|
194
202
|
const trigger = options.trigger ?? 'manual';
|
|
195
203
|
const strategy = options.strategy ?? 'deterministic';
|
|
196
204
|
createSessionStore(cwd, sessionId).append({
|
|
197
205
|
type: 'compact',
|
|
198
|
-
summary:
|
|
199
|
-
estimatedTokens:
|
|
200
|
-
sourceEventCount:
|
|
206
|
+
summary: compactResult.summary,
|
|
207
|
+
estimatedTokens: compactResult.estimatedTokens,
|
|
208
|
+
sourceEventCount: compactResult.sourceEventCount,
|
|
201
209
|
trigger,
|
|
202
210
|
strategy,
|
|
203
211
|
});
|
|
204
212
|
return {
|
|
205
|
-
...
|
|
213
|
+
...compactResult,
|
|
206
214
|
sessionId,
|
|
207
215
|
trigger,
|
|
208
216
|
strategy,
|