@wangzhizhi/remi 0.1.225 → 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.
Files changed (36) hide show
  1. package/README.md +206 -2
  2. package/dist/help.js +1 -1
  3. package/dist/initPrompt.js +1 -1
  4. package/dist/setup.js +5 -4
  5. package/dist/statusCard.js +1 -1
  6. package/dist/statusline.js +1 -1
  7. package/dist/tui/RemiApp.js +55 -7
  8. package/dist/tui/hooksPanel.js +2 -2
  9. package/dist/tui/renderers/MessageList.js +10 -7
  10. package/dist/version.js +1 -1
  11. package/node_modules/@remi/compact/dist/index.js +13 -5
  12. package/node_modules/@remi/compact/package.json +1 -1
  13. package/node_modules/@remi/config/dist/index.js +816 -38
  14. package/node_modules/@remi/config/package.json +1 -1
  15. package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
  16. package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
  17. package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
  18. package/node_modules/@remi/core/dist/index.js +630 -33
  19. package/node_modules/@remi/core/dist/stableHash.js +30 -0
  20. package/node_modules/@remi/core/package.json +1 -1
  21. package/node_modules/@remi/hooks/package.json +1 -1
  22. package/node_modules/@remi/llm/package.json +1 -1
  23. package/node_modules/@remi/mcp/dist/index.js +32 -18
  24. package/node_modules/@remi/mcp/package.json +1 -1
  25. package/node_modules/@remi/memory/package.json +1 -1
  26. package/node_modules/@remi/permissions/package.json +1 -1
  27. package/node_modules/@remi/sessions/dist/index.js +38 -0
  28. package/node_modules/@remi/sessions/package.json +1 -1
  29. package/node_modules/@remi/skills/dist/index.js +2 -2
  30. package/node_modules/@remi/skills/package.json +1 -1
  31. package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
  32. package/node_modules/@remi/terminal-markdown/package.json +1 -1
  33. package/node_modules/@remi/tools/dist/index.js +1 -1
  34. package/node_modules/@remi/tools/package.json +1 -1
  35. package/package.json +13 -13
  36. package/prompt/tool-use-system.md +19 -3
package/README.md CHANGED
@@ -1,9 +1,213 @@
1
1
  # Remi
2
2
 
3
- Remi AI coding agent CLI.
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.225
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.json',
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',
@@ -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 key files such as README, package manifests, workspace config, build/test config, Makefile, CI config, and any existing AI-agent instruction files: AGENTS.md, AGENT.md, REMI.md, .cursor/rules, .cursorrules, .github/copilot-instructions.md, .windsurfrules, or .clinerules.
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.json if it does not exist',
30
- ' remi setup --force Rewrite ~/.remi/config.json with the default preset',
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 }) {
@@ -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';
@@ -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' },
@@ -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 { compactSession } from '@remi/compact';
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 = compactSession(cwd, sessionId);
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) {
@@ -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.json 后重新打开 /hooks。'
19
- : 'No hooks configured. Add hooks in config.json, then reopen /hooks.',
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
- 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: slashCommand.rest })] })) : (_jsx(Text, { color: remiDarkTheme.text, wrap: "wrap", children: text }))] }));
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 descriptionWidth = Math.max(12, width - markerWidth - labelWidth);
1341
- return { markerWidth, labelWidth, descriptionWidth };
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
@@ -1,5 +1,5 @@
1
1
  import { t } from './i18n.js';
2
- export const version = '0.1.225';
2
+ export const version = '0.1.244';
3
3
  export function formatVersionMessage(language) {
4
4
  return t(language, 'version.message', { version });
5
5
  }
@@ -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 ?? freshSessionMemoryForCompact(cwd, sessionId, options);
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: result.summary,
199
- estimatedTokens: result.estimatedTokens,
200
- sourceEventCount: result.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
- ...result,
213
+ ...compactResult,
206
214
  sessionId,
207
215
  trigger,
208
216
  strategy,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/compact",
3
- "version": "0.1.225",
3
+ "version": "0.1.244",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"