pi-minimalist 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.fr.md ADDED
@@ -0,0 +1,86 @@
1
+ # pi-minimalist
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md) | **Français**
4
+
5
+ > Traduction du [README.md](README.md) anglais ; en cas de divergence, la version anglaise fait foi. Les commandes, les noms de réglages et les clés JSON restent en anglais, pour que vous puissiez les retrouver dans l'interface.
6
+
7
+ **Moins de défilement. Plus de place pour la réponse.**
8
+
9
+ Un historique plus calme pour [Pi](https://pi.dev) : les appels d'outils volumineux deviennent des résumés d'une ligne, et vous ouvrez la sortie complète quand vous en avez besoin.
10
+
11
+ ## Voir la différence
12
+
13
+ **Avant :** un seul appel d'outil occupe presque tout l'écran.
14
+
15
+ <img width="656" height="550" alt="before" src="https://github.com/user-attachments/assets/aa1602f7-5db1-49b8-a4d3-cbd1aef69ea0" />
16
+
17
+ **Après :** le même appel, sur une ligne.
18
+
19
+ <img width="699" height="50" alt="after" src="https://github.com/user-attachments/assets/c0e8bca4-afa7-42c1-9bf0-69f267d18d9f" />
20
+
21
+ **Cliquez** sur une ligne repliée pour l'ouvrir en plusieurs lignes. Cliquez sur l'une d'elles pour n'afficher que sa sortie. Cliquez sur la ligne `▾ Expanded · click to fold` au-dessus d'une série ouverte pour la replier.
22
+
23
+ <img src="docs/click.png" width="600" alt="Un clic sur une ligne repliée l'ouvre en lignes ; un clic sur une ligne n'ouvre que celle-ci">
24
+
25
+ Appuyez sur **Ctrl+O** pour développer la sortie des outils, y compris les diffs et la coloration syntaxique. Les lignes développées renvoient aussi à la ligne les longues commandes au lieu de les couper. Rien n'est supprimé de la session ni caché au modèle : seul l'affichage change.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pi install npm:pi-minimalist
31
+ ```
32
+
33
+ Redémarrez Pi : les appels d'outils s'affichent en lignes compactes. Aucune configuration n'est nécessaire.
34
+
35
+ ## Choisir le niveau de discrétion
36
+
37
+ Lancez **`/minimalist`** pour modifier les réglages en direct. Commencez par les lignes d'outils compactes, ou allez plus loin :
38
+
39
+ - **Combine consecutive tool calls** (regrouper les appels d'outils consécutifs) en un seul résumé, par exemple `read ×2, edit ×1`.
40
+ - **Collapse earlier activity** (replier l'activité précédente) pour garder la dernière réponse au premier plan. L'activité qui la précède est remplacée par le temps de travail ou le nombre d'outils.
41
+ - **Compact thinking rows** (lignes de réflexion compactes), ou garder la réflexion visible pendant qu'elle arrive.
42
+ - **Keep running tools visible** (garder visibles les outils en cours) pendant le regroupement des autres appels, avec un minuteur qui affiche leur durée.
43
+ - **Excluded tools** (outils exclus) : un outil donné (par exemple `subagent`) garde sa propre carte au lieu d'être réduit à une ligne. Ouvrez cette ligne dans `/minimalist`, tapez pour chercher, puis appuyez sur Entrée sur un outil pour le basculer.
44
+
45
+ Une nouvelle installation démarre avec le préréglage **`full`** (voir plus bas) : lignes d'outils compactes, appels regroupés et lignes de réflexion compactes. Le repli de l'activité est facultatif. Choisissez `lite` si vous préférez voir chaque appel séparément.
46
+
47
+ ### Préréglages
48
+
49
+ La ligne **Preset** en haut de `/minimalist` règle quatre options à la fois : **Compact tool rows**, **Combine consecutive tool calls**, **Collapse earlier activity** et **Compact thinking rows**.
50
+
51
+ | Préréglage | Ce que vous obtenez |
52
+ | --- | --- |
53
+ | `off` | Les cartes d'outils natives de Pi. |
54
+ | `lite` | Des lignes d'outils d'une ligne, chaque appel sur sa propre ligne. |
55
+ | `full` | `lite`, plus les appels consécutifs regroupés et les lignes de réflexion compactes. |
56
+ | `max` | `full`, plus l'activité précédente repliée (`Worked for …`). |
57
+ | `custom` | Vos propres valeurs pour ces quatre options. |
58
+
59
+ <img src="docs/presets.png" width="600" alt="L'écran de choix du préréglage">
60
+
61
+ Un préréglage n'écrase jamais vos réglages. Tant que `off`, `lite`, `full` ou `max` est sélectionné, seules ces quatre lignes sont grisées et affichent les valeurs du préréglage ; appuyer sur Entrée dessus ne fait rien. Toutes les autres options (Left border, Elapsed timer, Symbols, etc.) restent à vous et restent modifiables. Vos propres valeurs sont conservées et reviennent quand vous choisissez `custom`. La première fois que vous choisissez `custom`, il part de `lite`.
62
+
63
+ **Mise à jour :** si vos réglages contiennent déjà un bloc `minimalist`, vous restez sur `custom` et rien ne change. Seul un utilisateur sans aucun réglage `minimalist` démarre sur `full`, ce qui rend les lignes de réflexion compactes. Pour choisir un autre aspect, changez **Preset**.
64
+
65
+ ### Commandes courantes
66
+
67
+ | Commande | Action |
68
+ | --- | --- |
69
+ | `/minimalist` | Ouvrir les réglages (`/minimalist config` fonctionne aussi) |
70
+ | `/minimalist status` | Afficher les réglages actuels |
71
+ | Ctrl+O | Développer ou replier la sortie des outils |
72
+ | Ctrl+T | Développer ou replier la réflexion |
73
+
74
+ Les réglages s'appliquent à l'historique existant et sont enregistrés globalement. Si un symbole s'affiche mal dans votre terminal, choisissez **Symbols → ASCII**. Aucune Nerd Font n'est requise.
75
+
76
+ ## Revenir à l'affichage habituel de Pi
77
+
78
+ Désactivez **Compact tool rows** pour retrouver les cartes d'outils natives. Ou choisissez le préréglage `off`.
79
+
80
+ Pour désactiver complètement l'extension, lancez `pi config`, désactivez pi-minimalist, puis redémarrez Pi.
81
+
82
+ ## Compatibilité
83
+
84
+ Testé avec Pi **1.0.0**. Les mises à jour de Pi peuvent affecter la compatibilité du rendu ; [signalez un problème](https://github.com/EviHex/pi-minimalist/issues) en indiquant votre version de Pi, votre terminal et une capture d'écran.
85
+
86
+ [Licence MIT](LICENSE).
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-minimalist
2
2
 
3
+ **English** | [简体中文](README.zh-CN.md) | [Français](README.fr.md)
4
+
3
5
  **Less scrolling. More room for the answer.**
4
6
 
5
7
  A quieter transcript for [Pi](https://pi.dev): turn bulky tool calls into one-line summaries, and open the full output whenever you need it.
@@ -36,6 +38,7 @@ Run **`/minimalist`** to change settings live. Start with compact tool rows, or
36
38
  - **Collapse earlier activity** so the latest reply stays in focus. Replace the preceding activity with elapsed work time or tool counts.
37
39
  - **Compact thinking rows**, or keep thinking visible as it arrives.
38
40
  - **Keep running tools visible** while grouping other calls, with an elapsed timer to show how long they take.
41
+ - **Excluded tools**: let chosen tools (for example `subagent`) keep their own card instead of a one-line row. Open the row in `/minimalist`, type to search, and press Enter on a tool to flip it.
39
42
 
40
43
  A fresh install starts on the **`full`** preset (see below): compact tool rows, grouped calls and compact thinking. Activity folding is opt-in. Choose `lite` if you prefer to see each call separately.
41
44
 
@@ -0,0 +1,86 @@
1
+ # pi-minimalist
2
+
3
+ [English](README.md) | **简体中文** | [Français](README.fr.md)
4
+
5
+ > 本文翻译自英文版 [README.md](README.md),如有出入以英文版为准。命令、设置名和 JSON 键保持英文原样,方便你在界面里对照。
6
+
7
+ **少滚动,给答案留出更多空间。**
8
+
9
+ 让 [Pi](https://pi.dev) 的对话记录更安静:把冗长的工具调用折成一行摘要,需要时再展开完整输出。
10
+
11
+ ## 效果对比
12
+
13
+ **之前:** 一次工具调用占掉大半个屏幕。
14
+
15
+ <img width="656" height="550" alt="before" src="https://github.com/user-attachments/assets/aa1602f7-5db1-49b8-a4d3-cbd1aef69ea0" />
16
+
17
+ **之后:** 同一次调用,只占一行。
18
+
19
+ <img width="699" height="50" alt="after" src="https://github.com/user-attachments/assets/c0e8bca4-afa7-42c1-9bf0-69f267d18d9f" />
20
+
21
+ **点击**折叠的一行,会把它展开成多行。点击其中一行,只看这一行的输出。点击展开区域上方的 `▾ Expanded · click to fold`,可以重新折叠。
22
+
23
+ <img src="docs/click.png" width="600" alt="点击折叠的一行会展开成多行;点击其中一行只展开这一行">
24
+
25
+ 按 **Ctrl+O** 展开工具输出,包括 diff 和语法高亮。展开后的行会自动换行显示长命令,不会截断。会话内容不会被删除,模型也看得到全部内容;只有显示方式改变。
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pi install npm:pi-minimalist
31
+ ```
32
+
33
+ 重启 Pi,工具调用就会显示为紧凑的一行。不需要任何配置。
34
+
35
+ ## 选择你想要的安静程度
36
+
37
+ 运行 **`/minimalist`** 可以实时修改设置。可以只用紧凑工具行,也可以走得更远:
38
+
39
+ - **Combine consecutive tool calls**(合并连续工具调用):合并成一条摘要,例如 `read ×2, edit ×1`。
40
+ - **Collapse earlier activity**(折叠早先的活动):让最新的回复保持在视线中心。之前的活动会被替换成耗时或工具计数。
41
+ - **Compact thinking rows**(紧凑思考行),或让思考内容在生成时保持可见。
42
+ - **Keep running tools visible**(保持运行中的工具可见):在合并其他调用的同时,运行中的工具会带计时器显示已耗时间。
43
+ - **Excluded tools**(排除的工具):让指定工具(例如 `subagent`)保留自己的卡片,不压缩成一行。在 `/minimalist` 里打开该行,输入关键字搜索,在工具上按 Enter 即可切换。
44
+
45
+ 全新安装默认使用 **`full`** 预设(见下文):紧凑工具行、合并调用和紧凑思考行。活动折叠需要手动开启。如果你想逐条看到每次调用,请选择 `lite`。
46
+
47
+ ### 预设
48
+
49
+ `/minimalist` 顶部的 **Preset** 一行,可以一次设置四个选项:**Compact tool rows**、**Combine consecutive tool calls**、**Collapse earlier activity** 和 **Compact thinking rows**。
50
+
51
+ | 预设 | 效果 |
52
+ | --- | --- |
53
+ | `off` | Pi 原生的工具卡片。 |
54
+ | `lite` | 单行工具行,每次调用各占一行。 |
55
+ | `full` | `lite` 加上合并连续调用和紧凑思考行。 |
56
+ | `max` | `full` 加上折叠早先的活动(`Worked for …`)。 |
57
+ | `custom` | 这四个选项使用你自己的值。 |
58
+
59
+ <img src="docs/presets.png" width="600" alt="预设选择界面">
60
+
61
+ 预设不会覆盖你的设置。选中 `off`、`lite`、`full` 或 `max` 时,只有那四行会变灰并显示预设的值,在它们上面按 Enter 没有任何作用。其他选项(Left border、Elapsed timer、Symbols 等)仍然归你所有,可以继续编辑。你自己的值会被保留,选回 `custom` 时会恢复。第一次选择 `custom` 时,会从 `lite` 开始。
62
+
63
+ **升级说明:** 如果你的设置里已经有 `minimalist` 配置块,你会继续停留在 `custom`,什么都不会变。只有完全没有 `minimalist` 设置的用户才会从 `full` 开始,这会让思考行变得紧凑。想换一种外观,修改 **Preset** 即可。
64
+
65
+ ### 日常操作
66
+
67
+ | 操作 | 作用 |
68
+ | --- | --- |
69
+ | `/minimalist` | 打开设置(`/minimalist config` 也可以) |
70
+ | `/minimalist status` | 显示当前设置 |
71
+ | Ctrl+O | 展开或折叠工具输出 |
72
+ | Ctrl+T | 展开或折叠思考内容 |
73
+
74
+ 设置会作用于已有的对话历史,并保存为全局设置。如果某个符号在你的终端里显示不正常,请选择 **Symbols → ASCII**。不需要 Nerd Font。
75
+
76
+ ## 回到 Pi 的默认外观
77
+
78
+ 关闭 **Compact tool rows** 即可恢复原生工具卡片。也可以选择 `off` 预设。
79
+
80
+ 要完全禁用这个扩展,运行 `pi config`,禁用 pi-minimalist,然后重启 Pi。
81
+
82
+ ## 兼容性
83
+
84
+ 已在 Pi **1.0.0** 上测试。Pi 的更新可能影响渲染兼容性;请[反馈问题](https://github.com/EviHex/pi-minimalist/issues),并附上你的 Pi 版本、终端和一张截图。
85
+
86
+ [MIT 许可证](LICENSE)。
package/index.ts CHANGED
@@ -22,7 +22,7 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
22
22
  // keybindings and colours, as Pi's own `/settings`.
23
23
  import { SettingsList } from "@earendil-works/pi-tui";
24
24
  import { installBridges, sharedState } from "./src/bridge.ts";
25
- import { type BasicKey, type BasicSettings, type Config } from "./src/config.ts";
25
+ import { type Config, type EditableSettings } from "./src/config.ts";
26
26
  import { customSeed, loadSettings, saveBasicSettings } from "./src/config-file.ts";
27
27
  import { argumentCompletions, createConfigScreen, summary } from "./src/config-ui.ts";
28
28
  import { patchCore } from "./src/core-patch.ts";
@@ -34,7 +34,11 @@ export default function (pi: ExtensionAPI) {
34
34
  // strand every row on stale state.
35
35
  const { config, grouping } = sharedState(loadSettings());
36
36
 
37
- installBridges({ config, grouping });
37
+ // A timer (the post-stream thinking linger) must repaint outside any render.
38
+ // Extensions get no TUI handle except through a factory, so an invisible
39
+ // widget (zero lines, below the editor, no spacer) captures it.
40
+ let tui: { requestRender(): void } | undefined;
41
+ installBridges({ config, grouping, requestRender: () => tui?.requestRender() });
38
42
  // Idempotent, so a /reload only refreshes the Bridge the wrappers read.
39
43
  patchCore(core);
40
44
 
@@ -46,7 +50,19 @@ export default function (pi: ExtensionAPI) {
46
50
  // restart. Session overrides are re-applied by Config.replace().
47
51
  config.replace(loadSettings());
48
52
  };
49
- pi.on("session_start", refresh);
53
+ pi.on("session_start", (_event, ctx) => {
54
+ refresh();
55
+ if (ctx.hasUI && ctx.mode === "tui") {
56
+ ctx.ui.setWidget(
57
+ "pi-minimalist-repaint",
58
+ (captured) => {
59
+ tui = captured;
60
+ return { render: () => [], invalidate() {} };
61
+ },
62
+ { placement: "belowEditor" },
63
+ );
64
+ }
65
+ });
50
66
  pi.on("turn_start", refresh);
51
67
  pi.on("agent_start", () => grouping.agentStarted());
52
68
  pi.on("agent_settled", () => grouping.agentSettled());
@@ -75,6 +91,8 @@ export default function (pi: ExtensionAPI) {
75
91
  theme: getSettingsListTheme(),
76
92
  config,
77
93
  onChange: (key, value) => persist(ctx, config, key, value),
94
+ toolNames: () => pi.getAllTools().map((tool) => tool.name),
95
+ onExcludeChange: (names) => persist(ctx, config, "excludeTools", names),
78
96
  onClose: () => done(),
79
97
  }),
80
98
  );
@@ -84,17 +102,17 @@ export default function (pi: ExtensionAPI) {
84
102
 
85
103
  type NotifyContext = { ui: { notify(message: string, type?: "info" | "warning" | "error"): void } };
86
104
 
87
- function persist(
105
+ function persist<K extends keyof EditableSettings>(
88
106
  ctx: NotifyContext,
89
107
  config: Config,
90
- key: BasicKey,
91
- value: BasicSettings[BasicKey],
108
+ key: K,
109
+ value: EditableSettings[K],
92
110
  ): void {
93
111
  // The first switch to `custom` starts from the `lite` look (see customSeed).
94
112
  const seed = key === "preset" && value === "custom" ? customSeed() : undefined;
95
- const changes: Partial<Record<BasicKey, unknown>> = { ...seed, [key]: value };
96
- const result = saveBasicSettings(changes as Partial<BasicSettings>);
97
- for (const [k, v] of Object.entries(changes) as [BasicKey, never][]) {
113
+ const changes: Partial<Record<keyof EditableSettings, unknown>> = { ...seed, [key]: value };
114
+ const result = saveBasicSettings(changes as Partial<EditableSettings>);
115
+ for (const [k, v] of Object.entries(changes) as [keyof EditableSettings, never][]) {
98
116
  config.set(k, v);
99
117
  // On success the file agrees, so it becomes the source of truth again.
100
118
  if (result.ok) config.clearSessionOverride(k);
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "pi-minimalist",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Compact one-line tool and thinking rows for Pi's interactive transcript",
5
- "keywords": ["pi-package"],
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
6
8
  "license": "MIT",
7
9
  "author": "Pinglei He",
8
10
  "repository": {
@@ -10,13 +12,20 @@
10
12
  "url": "git+https://github.com/EviHex/pi-minimalist.git"
11
13
  },
12
14
  "type": "module",
13
- "files": ["index.ts", "src/", "README.md", "LICENSE"],
15
+ "files": [
16
+ "index.ts",
17
+ "src/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
14
21
  "peerDependencies": {
15
22
  "@earendil-works/pi-coding-agent": "*",
16
23
  "@earendil-works/pi-tui": "*"
17
24
  },
18
25
  "pi": {
19
- "extensions": ["./index.ts"],
26
+ "extensions": [
27
+ "./index.ts"
28
+ ],
20
29
  "image": "https://raw.githubusercontent.com/EviHex/pi-minimalist/main/docs/click.png"
21
30
  }
22
31
  }
package/src/bridge.ts CHANGED
@@ -16,7 +16,7 @@ import { RunGrouping } from "./run-grouping.ts";
16
16
  import { Painter, type Row, type ThemeLike } from "./row.ts";
17
17
  import { createToolRenderer, type ToolRenderer } from "./tool-renderer.ts";
18
18
  import { createThinkingPreview, singleHueThinkingTheme, type ThinkingPreview } from "./thinking.ts";
19
- import type { Timers } from "./components.ts";
19
+ import { realTimers, type Timers } from "./components.ts";
20
20
 
21
21
  export const BRIDGE_SYMBOLS = {
22
22
  /** The `Bridge` object: everything the prototype wrappers call. */
@@ -42,6 +42,11 @@ export type Bridge = {
42
42
  compactThinking: () => boolean;
43
43
  /** Keep a STREAMING thinking block expanded. */
44
44
  keepActiveThinkingExpanded: () => boolean;
45
+ /**
46
+ * Run `callback` once, `THINKING_LINGER_MS` after a streamed message ends,
47
+ * then repaint. Keeps a short thought on screen long enough to be read.
48
+ */
49
+ afterThinkingLinger: (callback: () => void) => void;
45
50
 
46
51
  /** Chronology hooks that keep thinking and prose in transcript order. */
47
52
  observeThinking: (owner: object, runIndex: number, streaming: boolean, hidden: boolean, timestamp?: number) => string;
@@ -103,8 +108,13 @@ export function sharedState(settings?: ConstructorParameters<typeof Config>[0]):
103
108
  export type InstallOptions = SharedState & {
104
109
  /** Injectable repaint scheduler; production uses Node's timers. */
105
110
  timers?: Timers;
111
+ /** Repaint the TUI from outside a render (a timer). No-op without a TUI. */
112
+ requestRender?: () => void;
106
113
  };
107
114
 
115
+ /** How long expanded thinking stays open after its message stops streaming. */
116
+ export const THINKING_LINGER_MS = 3000;
117
+
108
118
  /**
109
119
  * Install the bridge.
110
120
  *
@@ -112,7 +122,7 @@ export type InstallOptions = SharedState & {
112
122
  * from `session_shutdown`: reload ordering can let an old shutdown hook erase the
113
123
  * newly installed bridge. Process exit clears globalThis naturally.
114
124
  */
115
- export function installBridges({ config, grouping, timers }: InstallOptions): void {
125
+ export function installBridges({ config, grouping, timers = realTimers, requestRender }: InstallOptions): void {
116
126
  const globals = globalThis as Globals;
117
127
  const painter = (theme: ThemeLike) => new Painter(theme, config);
118
128
 
@@ -122,6 +132,14 @@ export function installBridges({ config, grouping, timers }: InstallOptions): vo
122
132
  thinkingMarkdownTheme: (base, theme) => singleHueThinkingTheme(base, theme, config),
123
133
  compactThinking: () => config.get("thinkingAsToolCall") === true,
124
134
  keepActiveThinkingExpanded: () => config.get("keepActiveThinkingExpanded"),
135
+ afterThinkingLinger: (callback) => {
136
+ // Timers only has intervals; clearing on the first tick makes it one-shot.
137
+ const handle = timers.setInterval(() => {
138
+ timers.clearInterval(handle);
139
+ callback();
140
+ requestRender?.();
141
+ }, THINKING_LINGER_MS);
142
+ },
125
143
 
126
144
  // Chronology: core walks message content in transcript order, and run
127
145
  // folding depends on that order.
@@ -25,6 +25,7 @@ import {
25
25
  DEFAULT_BASIC,
26
26
  DEFAULTS,
27
27
  DEFAULT_TOKENS,
28
+ EDITABLE_KEYS,
28
29
  GLYPH_PRESETS,
29
30
  FRESH_PRESET,
30
31
  LOOK_KEYS,
@@ -32,7 +33,7 @@ import {
32
33
  isFresh,
33
34
  isPreset,
34
35
  SETTINGS_KEY,
35
- type BasicSettings,
36
+ type EditableSettings,
36
37
  type Glyphs,
37
38
  type Settings,
38
39
  type Tokens,
@@ -176,13 +177,14 @@ export type SaveResult =
176
177
  /**
177
178
  * Persist only the supplied editor-managed settings into the global agent file.
178
179
  * A normal edit passes one key.
179
- * Hand-written `glyphs`/`tokens`/`excludeTools` values survive untouched.
180
+ * Hand-written `glyphs`/`tokens` values survive untouched, and so does a hand-written
181
+ * `excludeTools` unless the tool picker is what changed it.
180
182
  *
181
183
  * REFUSES to write a file containing comments. `JSON.stringify` would silently
182
184
  * delete them, and quietly destroying an annotated config is far worse than
183
185
  * asking the user to edit one line by hand.
184
186
  */
185
- export function saveBasicSettings(settings: Partial<BasicSettings>, env = process.env): SaveResult {
187
+ export function saveBasicSettings(settings: Partial<EditableSettings>, env = process.env): SaveResult {
186
188
  const path = globalSettingsPath(env);
187
189
 
188
190
  let raw = "";
@@ -208,7 +210,7 @@ export function saveBasicSettings(settings: Partial<BasicSettings>, env = proces
208
210
  const merged: Json = { ...existing };
209
211
  // The first write must pin the fresh-install preset, or the file would stop looking fresh and become `custom`.
210
212
  if (isFresh(existing)) merged.preset = FRESH_PRESET;
211
- for (const key of BASIC_KEYS) {
213
+ for (const key of EDITABLE_KEYS) {
212
214
  if (settings[key] !== undefined) merged[key] = settings[key];
213
215
  }
214
216
 
package/src/config-ui.ts CHANGED
@@ -71,7 +71,7 @@ export const FIELDS: Field[] = [
71
71
  {
72
72
  key: "keepActiveThinkingExpanded",
73
73
  label: "Show thinking while generating",
74
- description: "Show the full thinking block as it arrives, instead of a one-line preview.",
74
+ description: "Show the full thinking block as it arrives (and for 3s after), instead of a one-line preview.",
75
75
  },
76
76
  {
77
77
  key: "glyphStyle",
@@ -84,8 +84,14 @@ type InputComponent = Component & { handleInput(data: string): void };
84
84
  type SettingsListConstructor = new (...args: ConstructorParameters<typeof SettingsList>) => SettingsList;
85
85
  type BasicValue = BasicSettings[BasicKey];
86
86
 
87
+ /** `custom` is the only preset whose look rows you can edit; say so where it is shown. */
88
+ const CUSTOM_LABEL = "custom (editable)";
89
+ const presetLabel = (preset: string) => (preset === "custom" ? CUSTOM_LABEL : preset);
90
+ /** Back from the displayed text to the stored preset name. */
91
+ const presetFromLabel = (text: string) => (text === CUSTOM_LABEL ? "custom" : text);
92
+
87
93
  const CHOICES: Partial<Record<BasicKey, string[]>> = {
88
- preset: [...PRESET_NAMES],
94
+ preset: PRESET_NAMES.map(presetLabel),
89
95
  activitySummary: ["elapsed time", "tools used"],
90
96
  glyphStyle: ["Unicode", "ASCII"],
91
97
  };
@@ -99,10 +105,13 @@ export function items(settings: Settings, dim?: (text: string) => string): Setti
99
105
  const value = displayValue(settings[key]);
100
106
  // SettingsList has no disabled rows, so a locked row shows its dimmed value
101
107
  // as its ONLY value: Enter "cycles" to the same text and nothing changes.
108
+ // The label is dimmed too: ordinary values are already `muted`, which is too
109
+ // close to the hint colour to tell a locked row apart, while ordinary labels
110
+ // are the default colour.
102
111
  if (dim && isLook(key)) {
103
112
  const shown = dim(value);
104
- const hint = `Set by the '${settings.preset}' preset. Switch Preset to 'custom' to edit (your custom values are kept).`;
105
- return { id: key, label, description: hint, currentValue: shown, values: [shown] };
113
+ const hint = `Set by the '${settings.preset}' preset. Switch Preset to 'custom (editable)' to edit (your custom values are kept).`;
114
+ return { id: key, label: dim(label), description: hint, currentValue: shown, values: [shown] };
106
115
  }
107
116
  return { id: key, label, description, currentValue: value, values: CHOICES[key] ?? ["on", "off"] };
108
117
  },
@@ -114,9 +123,54 @@ export type ConfigScreenDeps = {
114
123
  theme: SettingsListTheme;
115
124
  config: Config;
116
125
  onChange: (key: BasicKey, value: BasicValue) => void;
126
+ /** Names of every tool known right now; read each time the picker opens, so it is never stale. */
127
+ toolNames: () => string[];
128
+ /** Called with the new `excludeTools` after every toggle in the picker. */
129
+ onExcludeChange: (names: string[]) => void;
117
130
  onClose: () => void;
118
131
  };
119
132
 
133
+ const EXCLUDE_ID = "excludeTools";
134
+ const PICKER_ROWS = 12;
135
+
136
+ /** "none", the names when there are one or two, else a count. */
137
+ export function excludedSummary(names: string[]): string {
138
+ if (names.length === 0) return "none";
139
+ return names.length <= 2 ? names.join(", ") : `${names.length} tools`;
140
+ }
141
+
142
+ /**
143
+ * One row per tool; Enter flips it between `compact` and `excluded`. It reuses
144
+ * SettingsList with its built-in search, so it has Pi's own keys and look and
145
+ * no list/filter code of ours. Names in `excludeTools` that no loaded tool
146
+ * carries (an extension not loaded this session) stay listed, or opening the
147
+ * picker would quietly forget them.
148
+ */
149
+ function toolPicker(deps: ConfigScreenDeps, done: (summary: string) => void): Component {
150
+ const { SettingsList, theme, config, toolNames, onExcludeChange } = deps;
151
+ let excluded = [...config.get("excludeTools")];
152
+ const known = new Set(toolNames());
153
+ const names = [...new Set([...known, ...excluded])].sort((a, b) => a.localeCompare(b));
154
+ const rows: SettingItem[] = names.map((name) => ({
155
+ id: name,
156
+ label: name,
157
+ description: known.has(name) ? undefined : "Not loaded in this session; kept in your settings.",
158
+ currentValue: excluded.includes(name) ? "excluded" : "compact",
159
+ values: ["compact", "excluded"],
160
+ }));
161
+ return new SettingsList(
162
+ rows,
163
+ Math.min(PICKER_ROWS, Math.max(rows.length, 1)),
164
+ theme,
165
+ (name, value) => {
166
+ excluded = value === "excluded" ? [...excluded, name] : excluded.filter((n) => n !== name);
167
+ onExcludeChange(excluded);
168
+ },
169
+ () => done(excludedSummary(excluded)),
170
+ { enableSearch: true },
171
+ );
172
+ }
173
+
120
174
  /** Build a live screen; toggling prose folding immediately adds/removes its two child rows. */
121
175
  export function createConfigScreen(deps: ConfigScreenDeps): InputComponent {
122
176
  const { SettingsList, theme, config, onChange, onClose } = deps;
@@ -126,7 +180,16 @@ export function createConfigScreen(deps: ConfigScreenDeps): InputComponent {
126
180
  const build = () => {
127
181
  const preset = config.all().preset;
128
182
  locked = preset !== "custom";
129
- const rows = items(config.all(), locked ? theme.hint : undefined);
183
+ const rows = [
184
+ ...items(config.all(), locked ? theme.hint : undefined),
185
+ {
186
+ id: EXCLUDE_ID,
187
+ label: "Excluded tools",
188
+ description: "Tools drawn with their own card instead of a one-line row, e.g. subagent. Enter to pick; type to search.",
189
+ currentValue: excludedSummary(config.get("excludeTools")),
190
+ submenu: (_current: string, done: (selected?: string) => void) => toolPicker(deps, done),
191
+ },
192
+ ];
130
193
  return new SettingsList(
131
194
  rows,
132
195
  rows.length,
@@ -134,7 +197,8 @@ export function createConfigScreen(deps: ConfigScreenDeps): InputComponent {
134
197
  (id, displayValue) => {
135
198
  if (!isBasicKey(id)) return;
136
199
  if (id === "preset") {
137
- const value = isPreset(displayValue) ? displayValue : "custom";
200
+ const picked = presetFromLabel(displayValue);
201
+ const value = isPreset(picked) ? picked : "custom";
138
202
  config.set(id, value);
139
203
  onChange(id, value);
140
204
  list = build();
@@ -182,7 +246,7 @@ function isBasicKey(id: string): id is BasicKey {
182
246
  }
183
247
 
184
248
  function displayValue(value: BasicValue): string {
185
- if (isPreset(value)) return value;
249
+ if (isPreset(value)) return presetLabel(value);
186
250
  if (value === "elapsed") return "elapsed time";
187
251
  if (value === "tools") return "tools used";
188
252
  if (value === "unicode") return "Unicode";
@@ -211,6 +275,7 @@ export function summary(settings: Settings): string {
211
275
  }),
212
276
  "",
213
277
  " /minimalist change these",
214
- " settings.json custom glyphs, colours, excluded tools",
278
+ ` Excluded tools ${excludedSummary(settings.excludeTools)}`,
279
+ " settings.json custom glyphs and colours",
215
280
  ].join("\n");
216
281
  }
package/src/config.ts CHANGED
@@ -125,7 +125,7 @@ export type BasicSettings = {
125
125
  glyphStyle: GlyphStyle;
126
126
  };
127
127
 
128
- /** Settings only reachable by editing settings.json by hand. */
128
+ /** Settings the `/minimalist` rows do not cover; `excludeTools` has a picker, the rest need settings.json. */
129
129
  export type AdvancedSettings = {
130
130
  /** Tool names left to their own renderer. Everything else is compacted. */
131
131
  excludeTools: string[];
@@ -161,10 +161,11 @@ export const PRESETS: Record<Exclude<Preset, "custom">, Record<LookKey, boolean>
161
161
  };
162
162
 
163
163
  export const DEFAULT_ADVANCED: AdvancedSettings = {
164
- // `subagent` ships a renderer showing run id, state and a ctrl+o hint that one
165
- // line cannot carry. Excluded by NAME rather than by a "does this tool
166
- // have its own renderer?" rule, which would silently exempt every third-party tool.
167
- excludeTools: ["subagent"],
164
+ // Empty by default: every tool is compacted. A tool whose own renderer carries
165
+ // more than one line can show (e.g. `subagent`: run id, state, ctrl+o hint)
166
+ // is opted out BY NAME here, never by a "does this tool have its own renderer?"
167
+ // rule, which would silently exempt every third-party tool.
168
+ excludeTools: [],
168
169
  glyphs: {},
169
170
  tokens: {},
170
171
  };
@@ -181,6 +182,10 @@ export const BASIC_KEYS = Object.keys(DEFAULT_BASIC) as (keyof BasicSettings)[];
181
182
 
182
183
  export type BasicKey = keyof BasicSettings;
183
184
 
185
+ /** What `/minimalist` may write: the basic keys plus the tool picker's `excludeTools`. */
186
+ export type EditableSettings = BasicSettings & Pick<AdvancedSettings, "excludeTools">;
187
+ export const EDITABLE_KEYS = [...BASIC_KEYS, "excludeTools"] as (keyof EditableSettings)[];
188
+
184
189
  // ---------------------------------------------------------------------------
185
190
  // Live configuration
186
191
  // ---------------------------------------------------------------------------
package/src/core-patch.ts CHANGED
@@ -264,7 +264,10 @@ function isMouseRegion(value: any): boolean {
264
264
  * `hideThinkingBlock` + the override map, then restoring both. Either way the
265
265
  * click handler stores into the LIVE map, so an explicit Ctrl+T expansion
266
266
  * always survives — the default only decides what happens when the user has
267
- * not expressed a preference.
267
+ * not expressed a preference. When a streamed message ends, the forced
268
+ * expansion LINGERS for `THINKING_LINGER_MS`, then the message re-renders
269
+ * collapsed: a one-phrase thought would otherwise vanish before it is read.
270
+ * Replayed history never streamed, so it never lingers.
268
271
  * - collapsed preview: swap the hidden run's `Text` for our compact line.
269
272
  * - expanded theme: recolor the run's `Markdown` in place (its private `theme`
270
273
  * field is read at render time, and this runs before the first render).
@@ -283,13 +286,26 @@ export function patchAssistantMessage(proto: AssistantProto): void {
283
286
  // its mouse layout, so Pi's y (relative to the lines we returned) would
284
287
  // miss the summary by that shift. Remember it to translate clicks back.
285
288
  const mouseShift = new WeakMap<AssistantProto, { shift: number; height: number }>();
289
+ // Messages seen streaming, and those still inside their post-stream linger.
290
+ const streamed = new WeakSet<AssistantProto>();
291
+ const lingering = new WeakSet<AssistantProto>();
286
292
 
287
293
  proto.updateContent = function (this: AssistantProto, message: any, isStreaming = this.isStreaming) {
288
294
  const savedHide = this.hideThinkingBlock;
289
295
  const savedOverrides = this.thinkingVisibilityOverrides;
296
+ const b = bridge();
297
+ const keepOpen = b?.keepActiveThinkingExpanded() === true;
298
+ if (isStreaming) streamed.add(this);
299
+ else if (streamed.delete(this) && keepOpen && b) {
300
+ lingering.add(this);
301
+ b.afterThinkingLinger(() => {
302
+ lingering.delete(this);
303
+ if (this.lastMessage) this.updateContent(this.lastMessage, false);
304
+ });
305
+ }
290
306
  // Default: a streaming block stays collapsed to its compact preview, which
291
307
  // already shows the newest text. Opt in to force it open instead.
292
- const forceOpen = isStreaming && bridge()?.keepActiveThinkingExpanded() === true;
308
+ const forceOpen = keepOpen && (isStreaming || lingering.has(this));
293
309
  if (forceOpen) {
294
310
  this.hideThinkingBlock = false;
295
311
  this.thinkingVisibilityOverrides = new Map();
package/src/row.ts CHANGED
@@ -197,11 +197,7 @@ export class Painter {
197
197
  const glyphs = this.config.glyphs();
198
198
  return counts
199
199
  .map(({ name, count }) => {
200
- // Thinking keeps its own hue inside a summary — unless it is configured to
201
- // behave exactly like a tool, in which case it should look like one too.
202
- const keepsThinkingHue =
203
- name === "think" && color === tokens.label && !this.config.get("thinkingAsToolCall");
204
- const token = keepsThinkingHue ? tokens.thinking : color;
200
+ const token = name === "think" && color === tokens.label ? tokens.thinking : color;
205
201
  return `${this.theme.fg(token, name)} ${this.theme.fg(tokens.details, `${glyphs.count}${count}`)}`;
206
202
  })
207
203
  .join(this.theme.fg(tokens.details, ", "));
package/src/thinking.ts CHANGED
@@ -49,7 +49,8 @@ export function createThinkingPreview(deps: ThinkingDeps): ThinkingPreview {
49
49
  return new Painter(theme, config).labeled({
50
50
  glyph: streaming ? glyphs.running : glyphs.done,
51
51
  label: "think",
52
- labelColor: tokens.label,
52
+ labelColor: tokens.thinking,
53
+ glyphColor: tokens.thinking,
53
54
  details: text.replace(/\s+/g, " ").trim(),
54
55
  });
55
56
  };