pi-one-ui 0.1.1 → 0.2.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.
Files changed (113) hide show
  1. package/README.md +66 -39
  2. package/extensions/{shell → app/commands}/settings-previews.ts +7 -7
  3. package/extensions/app/config/renderer.ts +13 -63
  4. package/extensions/app/config/shell.ts +24 -138
  5. package/extensions/app/config/store.ts +290 -0
  6. package/extensions/app/host/pi-extension-port.ts +25 -0
  7. package/extensions/app/host/pi-ui-port.ts +52 -0
  8. package/extensions/app/host/tui-capabilities.ts +27 -0
  9. package/extensions/app/overlay/input-router.ts +63 -0
  10. package/extensions/app/overlay/overlay-manager.ts +39 -0
  11. package/extensions/{shell → app/overlay}/selector-border.ts +3 -3
  12. package/extensions/app/overlay/selector-controller.ts +104 -0
  13. package/extensions/app/ownership/layout-registry.ts +65 -0
  14. package/extensions/app/ownership.ts +21 -7
  15. package/extensions/app/panel.ts +261 -140
  16. package/extensions/app/runtime/event-coordinator.ts +84 -0
  17. package/extensions/app/runtime/render-scheduler.ts +56 -0
  18. package/extensions/app/runtime/runtime-state.ts +83 -0
  19. package/extensions/app/runtime/tui-runtime.ts +546 -0
  20. package/extensions/features/{context.ts → context-inspector/index.ts} +30 -23
  21. package/extensions/features/{shell/flush-docked-bash.ts → flush-docked-bash.ts} +1 -4
  22. package/extensions/index.ts +2 -48
  23. package/extensions/layouts/context/index.ts +188 -0
  24. package/extensions/{shell → layouts/context/message}/user-message-styles.ts +2 -2
  25. package/extensions/{shell → layouts/context/message}/user-message.ts +2 -2
  26. package/extensions/{transcript → layouts/context}/renderer/compact-mode.ts +17 -13
  27. package/extensions/layouts/context/renderer/context-refresh.ts +79 -0
  28. package/extensions/{transcript → layouts/context}/renderer/default-mode.ts +3 -3
  29. package/extensions/{transcript → layouts/context}/renderer/index.ts +14 -14
  30. package/extensions/{transcript → layouts/context}/renderer/mouse/hover.ts +2 -2
  31. package/extensions/{transcript → layouts/context}/renderer/mouse/interaction.ts +26 -7
  32. package/extensions/{transcript → layouts/context}/renderer/mouse/layout.ts +1 -1
  33. package/extensions/{transcript → layouts/context}/renderer/mouse/packets.ts +3 -3
  34. package/extensions/{transcript → layouts/context}/renderer/mouse/scroll.ts +12 -7
  35. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-component.ts +2 -2
  36. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-edit-render.ts +1 -1
  37. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-highlight.ts +1 -1
  38. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-layout.ts +1 -1
  39. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-limits.ts +1 -1
  40. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-palette.ts +1 -1
  41. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-presentation.ts +1 -1
  42. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-renderer.ts +1 -1
  43. package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-write-render.ts +1 -1
  44. package/extensions/{transcript → layouts/context}/renderer/tool/diff/index.ts +2 -2
  45. package/extensions/{transcript → layouts/context}/renderer/tool/grouping.ts +4 -4
  46. package/extensions/{transcript → layouts/context}/renderer/tool/message-display.ts +6 -4
  47. package/extensions/{transcript → layouts/context}/renderer/tool/names.ts +2 -2
  48. package/extensions/{transcript → layouts/context}/renderer/tool/result.ts +3 -3
  49. package/extensions/{features/agent-summary → layouts/context/summary}/core.ts +1 -1
  50. package/extensions/{features → layouts/context/thinking}/compact-thinking.ts +7 -7
  51. package/extensions/{shell → layouts/editor}/accent-rail-editor.ts +8 -8
  52. package/extensions/{shell → layouts/editor}/completion-menu.ts +3 -6
  53. package/extensions/layouts/editor/controller.ts +613 -0
  54. package/extensions/{shell → layouts/editor}/editor-metadata-format.ts +6 -3
  55. package/extensions/layouts/editor/factory.ts +159 -0
  56. package/extensions/layouts/editor/index.ts +16 -0
  57. package/extensions/{shell → layouts/editor}/minimalist-editor.ts +4 -4
  58. package/extensions/layouts/editor/ownership.ts +103 -0
  59. package/extensions/{shell → layouts/editor}/ui.ts +2 -2
  60. package/extensions/layouts/footer/controller.ts +392 -0
  61. package/extensions/layouts/footer/data.ts +41 -0
  62. package/extensions/{shell → layouts/footer}/extension-status.ts +2 -2
  63. package/extensions/{shell → layouts/footer}/footer-layout.ts +1 -1
  64. package/extensions/{shell → layouts/footer}/footer.ts +11 -8
  65. package/extensions/layouts/footer/index.ts +24 -0
  66. package/extensions/layouts/header/index.ts +5 -0
  67. package/extensions/{features/shell → layouts/header}/startup-header.ts +105 -19
  68. package/extensions/layouts/working-line/controller.ts +291 -0
  69. package/extensions/{shell → layouts/working-line}/interaction-summary.ts +3 -3
  70. package/extensions/{shell → layouts/working-line}/working-line-spinners.ts +1 -1
  71. package/extensions/{shell → layouts/working-line}/working-line.ts +4 -4
  72. package/extensions/{shell → services}/live-context.ts +29 -1
  73. package/extensions/services/project-refresh.ts +374 -0
  74. package/extensions/{shell → services}/project-state.ts +5 -5
  75. package/extensions/{shell/state.ts → services/session-state.ts} +75 -5
  76. package/extensions/{shell → shared}/format.ts +4 -4
  77. package/extensions/tools/component-tree.ts +6 -6
  78. package/package.json +4 -4
  79. package/extensions/shell/index.ts +0 -1659
  80. package/extensions/shell/project-refresh.ts +0 -130
  81. package/extensions/shell/settings-command.ts +0 -2144
  82. package/extensions/transcript/index.ts +0 -65
  83. package/extensions/transcript/renderer/transcript-refresh.ts +0 -79
  84. /package/extensions/{shell → app/ownership}/prototype-patch-registry.ts +0 -0
  85. /package/extensions/{shell → app/runtime}/session-lifecycle.ts +0 -0
  86. /package/extensions/features/{shell/aliases.ts → aliases.ts} +0 -0
  87. /package/extensions/features/{shell → legacy}/working-message.ts +0 -0
  88. /package/extensions/features/{reference → session-reference}/index.ts +0 -0
  89. /package/extensions/features/{reference → session-reference}/session.ts +0 -0
  90. /package/extensions/features/{reference/subagent.ts → subagent-autocomplete.ts} +0 -0
  91. /package/extensions/{shell → layouts/context/message}/user-message-osc.ts +0 -0
  92. /package/extensions/{transcript → layouts/context}/renderer/markdown-enhance.ts +0 -0
  93. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ATTRIBUTION.md +0 -0
  94. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ansi-utils.ts +0 -0
  95. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-header.ts +0 -0
  96. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-inline.ts +0 -0
  97. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-parse.ts +0 -0
  98. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-text.ts +0 -0
  99. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/line-width-safety.ts +0 -0
  100. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/shiki-highlight.ts +0 -0
  101. /package/extensions/{transcript → layouts/context}/renderer/tool/diff/write-execution.ts +0 -0
  102. /package/extensions/{transcript → layouts/context}/renderer/tool/show-more-hint.ts +0 -0
  103. /package/extensions/{features/agent-summary → layouts/context/summary}/index.ts +0 -0
  104. /package/extensions/{shell → layouts/editor}/accent-rail-layout-patch.ts +0 -0
  105. /package/extensions/{shell → layouts/editor}/editor-transfer.ts +0 -0
  106. /package/extensions/{shell → layouts/footer}/footer-format.ts +0 -0
  107. /package/extensions/{shell → layouts/working-line}/working-line-messages.ts +0 -0
  108. /package/extensions/{shell/git.ts → services/git-data.ts} +0 -0
  109. /package/extensions/{shell/package-version.ts → services/package-data.ts} +0 -0
  110. /package/extensions/{shell/runtime.ts → services/runtime-data.ts} +0 -0
  111. /package/extensions/{shell → services}/telemetry.ts +0 -0
  112. /package/extensions/{shell → shared}/icons.ts +0 -0
  113. /package/extensions/{shell → shared}/style.ts +0 -0
package/README.md CHANGED
@@ -1,11 +1,6 @@
1
1
  # pi-one-ui
2
2
 
3
- `pi-one-ui` 是一个单一 Pi 插件包,将两个 UI 扩展合并为一个可自定义的源码仓库:
4
-
5
- - **Zentui**:Editor、User message、Working line、Selector border、Footer
6
- - **CC Style**:Tool renderer、Rich diff、Thinking、Compact transcript、鼠标交互和辅助功能
7
-
8
- 安装后只需要启用这个包,不需要再单独安装 `pi-zentui` 或 `pi-cc-extensions`。
3
+ `pi-one-ui` 是一个统一的 Pi TUI 插件包,将 Zentui 的终端外壳能力与 Claude Code 风格的对话内容渲染合并到一个插件中。
9
4
 
10
5
  ## 安装
11
6
 
@@ -23,23 +18,37 @@ pi install git:github.com/<your-account>/pi-one-ui
23
18
  /oneui
24
19
  ```
25
20
 
26
- `/oneui` 是唯一的公开管理命令,执行后直接打开融合后的交互面板。面板按 `Tab` / `Shift+Tab` 切换 Shell、Renderer、Features、Presets 分区。Shell 分区会显示 Editor 和 User Message 的实时样式预览;修改会立即写入统一配置,活动会话支持的样式也会自动重绘。
21
+ `/oneui` 是唯一的公开管理命令。设置面板按照 TUI 的视觉 Layout 划分:
27
22
 
28
23
  ```text
29
- ~/.pi/agent/pi-one-ui.json
24
+ Header → Context → WorkingLine → Editor → Footer → Features → Presets
30
25
  ```
31
26
 
32
- 面板中的详细配置仍可直接编辑 JSON,以便开发自定义字段。
27
+ 其中:
28
+
29
+ - **Header**:启动信息、Logo、快捷键提示。
30
+ - **Context**:对话内容区,包含 User Message、Assistant Message、Thinking、Tool、Diff、Summary 和鼠标交互。
31
+ - **WorkingLine**:Pi 的工作状态行、spinner、token/thought/elapsed 信息。
32
+ - **Editor**:输入编辑器、completion、metadata、Accent Rail 和 Minimalist 样式。
33
+ - **Footer**:目录、Git、runtime、token、cost、extension status 等信息。
34
+ - **Features**:Context Inspector、Session Reference、Subagent Autocomplete、Aliases 等行为能力。
35
+ - **Overlay**:设置面板和 Context Inspector 等临时浮层由统一 OverlayManager 跟踪。
36
+
37
+ ## 配置
33
38
 
34
- ## 配置结构
39
+ 配置文件:
40
+
41
+ ```text
42
+ ~/.pi/agent/pi-one-ui.json
43
+ ```
35
44
 
36
- Zentui 的配置保留在 JSON 根级,CC Style 的配置放在 `renderer` 下:
45
+ 当前仍兼容 v1 配置结构:
37
46
 
38
47
  ```json
39
48
  {
40
49
  "version": 1,
41
50
  "components": {
42
- "editor": { "enabled": true, "style": "accent-rail" },
51
+ "editor": { "enabled": true, "style": "opencode" },
43
52
  "userMessages": { "enabled": true, "style": "framed" },
44
53
  "workingLine": { "enabled": true },
45
54
  "footer": { "style": "starship" }
@@ -52,34 +61,44 @@ Zentui 的配置保留在 JSON 根级,CC Style 的配置放在 `renderer` 下
52
61
  }
53
62
  ```
54
63
 
55
- 完整字段可参考上游快照:
64
+ 所有读写已经经过统一 ConfigStore;旧的 `zentui.json`、`claude-code-style.json` 和 `pi-mine-ui.json` 会被兼容读取并保留。`enableWorkingMessage` 仍可被旧配置读取,但统一插件不会注册第二套 working-message 实现,WorkingLine 的唯一 owner 是本插件的 WorkingLine layout。
56
65
 
57
- - `vendor/pi-zentui/docs/configuration.md`
58
- - `vendor/pi-cc-extensions/README.md`
59
-
60
- Working line 的 owner 固定为 Zentui,因此默认关闭 CC Style 的 `enableWorkingMessage`,避免两个扩展争抢 Pi 的无 key working row。
61
-
62
- ## 来源与目录结构
63
-
64
- 本项目基于以下上游项目进行融合开发:
65
-
66
- - `pi-zentui`:提供 Shell chrome、Editor、Footer、Working line 等能力
67
- - `pi-cc-extensions`:提供 Tool renderer、Diff、Thinking 和 Transcript 能力
68
-
69
- 上游源码快照保存在 `vendor/` 中,仅用于追踪和同步。`extensions/` 按 `pi-one-ui` 的产品能力组织,不保留上游项目的目录结构:
66
+ ## 源码结构
70
67
 
71
68
  ```text
72
69
  extensions/
73
- index.ts # 唯一公开入口和 /oneui 命令
74
- app/ # 统一配置、面板、预设和应用装配
75
- shell/ # EditorFooter、Working line 等终端外壳能力
76
- transcript/ # Tool、Diff、Thinking 和消息渲染
77
- features/ # Context、Session reference、Agent summary 等功能
78
- tools/ # Patch、ANSI、组件树和其他底层能力
79
- vendor/ # 上游只读参考快照和版本记录
70
+ index.ts # 唯一插件入口,创建 TuiRuntime
71
+ app/
72
+ runtime/ # TuiRuntimestate、事件和调度
73
+ host/ # Pi API 的窄化 host ports
74
+ config/ # ConfigStore 与配置领域解析
75
+ ownership/ # Layout ownership
76
+ overlay/ # OverlayManager、InputRouter、selector
77
+ commands/ # /oneui and settings panel previews
78
+ layouts/
79
+ header/ # Header layout
80
+ context/ # 原 Transcript:对话内容区
81
+ message/ # User Message
82
+ thinking/ # Thinking
83
+ renderer/ # Tool、Diff、Markdown、Mouse
84
+ summary/ # Agent summary
85
+ working-line/ # WorkingLine 与 interaction summary
86
+ editor/ # Editor 及其样式实现
87
+ footer/ # Footer 及其布局/format/status
88
+ features/
89
+ aliases.ts
90
+ context-inspector/ # /context,不是 Context 对话区
91
+ flush-docked-bash.ts
92
+ legacy/ # standalone compatibility implementation
93
+ session-reference/
94
+ subagent-autocomplete.ts
95
+ services/ # Git、runtime、package、project、session 数据
96
+ shared/ # ANSI、format、style、icons 等共享实现
97
+ tools/ # 底层组件树、patch 和 terminal helpers
98
+ vendor/ # 只读上游参考快照
80
99
  ```
81
100
 
82
- 公共入口只有 `/oneui`。ShellTranscript 的内部 owner 由应用装配层统一管理,避免不同模块争抢同一个 Pi UI 接缝。
101
+ `TuiRuntime` 是唯一 composition root,并直接装配 Layout controllers、services shared lifecycle effects。standalone compatibility harness 只位于 `tests/support/`,不进入安装包。
83
102
 
84
103
  ## 开发
85
104
 
@@ -90,12 +109,20 @@ npm test
90
109
  npm run pack:check
91
110
  ```
92
111
 
93
- `npm test` 会同时运行:
112
+ 完整检查:
113
+
114
+ ```bash
115
+ npm run verify
116
+ ```
94
117
 
95
- - `node:test`:Transcript、工具渲染和融合入口测试
96
- - `Vitest`:Shell 及其生命周期、布局和配置测试
118
+ 测试按领域划分:
97
119
 
98
- 测试文件按产品能力命名为 `tests/shell-*`、`tests/transcript-*` `tests/one-ui-*`。
120
+ - `tests/context-*`:Context 内容区、Tool、Diff、Thinking 和鼠标行为。
121
+ - `tests/working-line-*`:WorkingLine 和 turn summary。
122
+ - `tests/editor-*`:Editor、completion、metadata、transfer 和 Accent Rail。
123
+ - `tests/footer-*`:Footer、Footer format/layout/status。
124
+ - `tests/services-*`:Git、runtime、project、session 和 telemetry。
125
+ - `tests/shell-*`:standalone compatibility tests for the remaining layout lifecycle glue。
99
126
 
100
127
  本地启动:
101
128
 
@@ -103,4 +130,4 @@ npm run pack:check
103
130
  npm run pi:dev
104
131
  ```
105
132
 
106
- 上游快照版本记录在 `ARCHITECTURE.md`。更新上游时,先在 `vendor/` 中获取新快照,再将对应源码同步到 `extensions/`,不要直接修改 vendor 参考目录。
133
+ 上游快照保存在 `vendor/`,仅用于追踪和同步。不要直接修改 `vendor/` `node_modules/`。
@@ -1,12 +1,12 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { renderAccentRailEditorFrame } from "./accent-rail-editor.ts";
4
- import type { PolishedTuiConfig } from "../app/config/shell.ts";
5
- import { sanitizeEditorMetadataText } from "./editor-metadata-format.ts";
6
- import { renderMinimalistFrame } from "./minimalist-editor.ts";
7
- import { safeThemeFg } from "./style.ts";
8
- import { renderPolishedEditorFrame } from "./ui.ts";
9
- import { renderUserMessageStyle } from "./user-message-styles.ts";
3
+ import { renderAccentRailEditorFrame } from "../../layouts/editor/accent-rail-editor.ts";
4
+ import type { PolishedTuiConfig } from "../config/shell.ts";
5
+ import { sanitizeEditorMetadataText } from "../../layouts/editor/editor-metadata-format.ts";
6
+ import { renderMinimalistFrame } from "../../layouts/editor/minimalist-editor.ts";
7
+ import { safeThemeFg } from "../../shared/style.ts";
8
+ import { renderPolishedEditorFrame } from "../../layouts/editor/ui.ts";
9
+ import { renderUserMessageStyle } from "../../layouts/context/message/user-message-styles.ts";
10
10
 
11
11
  export const SETTINGS_PREVIEW_MAX_WIDTH = 72;
12
12
  export const SETTINGS_PREVIEW_MAX_ROWS = 10;
@@ -1,7 +1,5 @@
1
- import type { CompactThinkingConfig } from "../../features/compact-thinking.ts";
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
1
+ import type { CompactThinkingConfig } from "../../layouts/context/thinking/compact-thinking.ts";
2
+ import { configStore, type ConfigRecord } from "./store.ts";
5
3
 
6
4
  export type CompactStyleMode = "on" | "compact" | "off";
7
5
 
@@ -67,58 +65,6 @@ export type Config = {
67
65
  enableAliases: boolean;
68
66
  };
69
67
 
70
- const AGENT_DIR =
71
- process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
72
- const CONFIG_PATH = join(AGENT_DIR, "pi-one-ui.json");
73
- const LEGACY_UNIFIED_CONFIG_PATH = join(AGENT_DIR, "pi-mine-ui.json");
74
- const LEGACY_CONFIG_PATH = join(AGENT_DIR, "claude-code-style.json");
75
- const LEGACY_ZENTUI_CONFIG_PATH = join(AGENT_DIR, "zentui.json");
76
-
77
- type ConfigRecord = Record<string, unknown>;
78
-
79
- function readJson(path: string): ConfigRecord | undefined {
80
- try {
81
- if (!existsSync(path)) return undefined;
82
- const value = JSON.parse(readFileSync(path, "utf8"));
83
- return value && typeof value === "object" && !Array.isArray(value)
84
- ? (value as ConfigRecord)
85
- : undefined;
86
- } catch {
87
- return undefined;
88
- }
89
- }
90
-
91
- /**
92
- * Keep the two upstream configuration schemas under one portable file. Zentui
93
- * owns the root fields; the CC renderer owns `renderer`. Existing installations
94
- * are imported once without deleting their legacy files.
95
- */
96
- function readUnifiedConfig(): ConfigRecord {
97
- const current = readJson(CONFIG_PATH) ?? readJson(LEGACY_UNIFIED_CONFIG_PATH);
98
- if (current) {
99
- if (!existsSync(CONFIG_PATH)) {
100
- try {
101
- writeFileSync(CONFIG_PATH, JSON.stringify(current, null, 2));
102
- } catch {
103
- // Keep using the legacy file in memory if migration cannot be written.
104
- }
105
- }
106
- return current;
107
- }
108
-
109
- const shell = readJson(LEGACY_ZENTUI_CONFIG_PATH) ?? {};
110
- const renderer = readJson(LEGACY_CONFIG_PATH);
111
- if (!renderer && Object.keys(shell).length === 0) return {};
112
-
113
- const migrated = { ...shell, version: 1, renderer: renderer ?? {} };
114
- try {
115
- writeFileSync(CONFIG_PATH, JSON.stringify(migrated, null, 2));
116
- } catch {
117
- // A read-only agent directory still uses the migrated in-memory value.
118
- }
119
- return migrated;
120
- }
121
-
122
68
  function rendererConfigFrom(record: ConfigRecord): ConfigRecord {
123
69
  const renderer = record.renderer;
124
70
  return renderer && typeof renderer === "object" && !Array.isArray(renderer)
@@ -422,22 +368,26 @@ export function formatConfigStatus(source: Config = config): string {
422
368
  /** 进程内唯一的活动配置对象。读取直接用 `config.xxx`;写入必须走 `updateConfig`。 */
423
369
  export const config: Config = loadConfig();
424
370
 
371
+ /**
372
+ * Loads and normalizes the renderer projection from the shared store.
373
+ */
425
374
  function loadConfig(): Config {
426
375
  try {
427
- const unified = readUnifiedConfig();
428
- return normalizeConfig(rendererConfigFrom(unified));
376
+ return normalizeConfig(rendererConfigFrom(configStore.read()));
429
377
  } catch {
430
378
  // Ignore bad config and fall back to defaults.
431
379
  }
432
380
  return { ...DEFAULT_CONFIG };
433
381
  }
434
382
 
383
+ /**
384
+ * Persists the in-memory renderer projection through the shared store.
385
+ */
435
386
  export function saveConfig() {
436
- const unified = readUnifiedConfig();
437
- writeFileSync(
438
- CONFIG_PATH,
439
- JSON.stringify({ ...unified, version: 1, renderer: config }, null, 2),
440
- );
387
+ configStore.update((record) => {
388
+ record.version = 1;
389
+ record.renderer = config;
390
+ });
441
391
  }
442
392
 
443
393
  /** 运行时配置写入的唯一入口:合并 + 规范化 + 持久化。 */
@@ -1,20 +1,3 @@
1
- import { randomUUID } from "node:crypto";
2
- import {
3
- closeSync,
4
- existsSync,
5
- fchmodSync,
6
- fsyncSync,
7
- lstatSync,
8
- openSync,
9
- readFileSync,
10
- realpathSync,
11
- renameSync,
12
- statSync,
13
- unlinkSync,
14
- writeFileSync,
15
- } from "node:fs";
16
- import { basename, dirname, join } from "node:path";
17
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
18
1
  import {
19
2
  ICON_GLYPH_KEYS,
20
3
  type IconGlyphs,
@@ -23,14 +6,20 @@ import {
23
6
  normalizeIconMode,
24
7
  type ResolvedIcons,
25
8
  resolveConfiguredIcons,
26
- } from "../../shell/icons.ts";
27
- import { isSupportedColorSpec } from "../../shell/style.ts";
28
- import { normalizeWorkingLineMessages } from "../../shell/working-line.ts";
29
- import { PI_WORKING_LINE_MESSAGES } from "../../shell/working-line-messages.ts";
9
+ } from "../../shared/icons.ts";
10
+ import { isSupportedColorSpec } from "../../shared/style.ts";
11
+ import { normalizeWorkingLineMessages } from "../../layouts/working-line/working-line.ts";
12
+ import { PI_WORKING_LINE_MESSAGES } from "../../layouts/working-line/working-line-messages.ts";
13
+ import {
14
+ configPath as defaultConfigPath,
15
+ configStore,
16
+ mutateConfigFile,
17
+ type ConfigRecord,
18
+ } from "./store.ts";
30
19
 
31
20
  export type ColorSpec = string;
32
21
  export type ColorSource = "theme" | "terminal";
33
- export type { IconMode } from "../../shell/icons.ts";
22
+ export type { IconMode } from "../../shared/icons.ts";
34
23
 
35
24
  export type ContextStyle = "text" | "gauge" | "text+gauge";
36
25
  export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
@@ -429,9 +418,7 @@ export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
429
418
  };
430
419
 
431
420
  /** Shared configuration file used by the pi-one-ui package. */
432
- export const configPath = join(getAgentDir(), "pi-one-ui.json");
433
- const legacyConfigPath = join(getAgentDir(), "pi-mine-ui.json");
434
- const legacyZentuiConfigPath = join(getAgentDir(), "zentui.json");
421
+ export const configPath = defaultConfigPath;
435
422
 
436
423
  const defaultFooterSegments: FooterSegmentsConfig = {
437
424
  cwd: true,
@@ -597,8 +584,6 @@ export const defaultConfig: PolishedTuiConfig = {
597
584
  extensionStatuses: defaultStarshipStyle.extensionStatuses,
598
585
  };
599
586
 
600
- type ConfigRecord = Record<string, unknown>;
601
-
602
587
  function isRecord(value: unknown): value is ConfigRecord {
603
588
  return value !== null && typeof value === "object" && !Array.isArray(value);
604
589
  }
@@ -977,102 +962,18 @@ function validFooterSegmentEntries(
977
962
  ) as Partial<FooterSegmentsConfig>;
978
963
  }
979
964
 
980
- type ConfigFileState =
981
- | { kind: "missing"; record: ConfigRecord; writePath: string }
982
- | { kind: "valid"; record: ConfigRecord; writePath: string; mode: number }
983
- | { kind: "corrupt"; error: unknown };
984
-
985
- function errorCode(error: unknown): string | undefined {
986
- return typeof error === "object" && error !== null && "code" in error
987
- ? String(error.code)
988
- : undefined;
989
- }
990
-
991
- function readConfigFileState(path: string): ConfigFileState {
992
- let writePath = path;
993
- try {
994
- const pathStat = lstatSync(path);
995
- if (pathStat.isSymbolicLink()) writePath = realpathSync(path);
996
- const targetStat = statSync(writePath);
997
- const parsed = JSON.parse(readFileSync(writePath, "utf8"));
998
- return isRecord(parsed)
999
- ? {
1000
- kind: "valid",
1001
- record: parsed,
1002
- writePath,
1003
- mode: targetStat.mode & 0o7777,
1004
- }
1005
- : {
1006
- kind: "corrupt",
1007
- error: new Error("top-level value must be a JSON object"),
1008
- };
1009
- } catch (error) {
1010
- if (errorCode(error) === "ENOENT") {
1011
- try {
1012
- lstatSync(path);
1013
- } catch (pathError) {
1014
- if (errorCode(pathError) === "ENOENT")
1015
- return { kind: "missing", record: {}, writePath: path };
1016
- }
1017
- }
1018
- return { kind: "corrupt", error };
1019
- }
1020
- }
1021
-
1022
- function writeConfigAtomically(
1023
- path: string,
1024
- record: ConfigRecord,
1025
- mode?: number,
1026
- ): void {
1027
- const tempPath = join(
1028
- dirname(path),
1029
- `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`,
1030
- );
1031
- let file: number | undefined;
1032
- try {
1033
- file = openSync(tempPath, "wx", mode ?? 0o666);
1034
- if (mode !== undefined) fchmodSync(file, mode);
1035
- writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
1036
- fsyncSync(file);
1037
- closeSync(file);
1038
- file = undefined;
1039
- renameSync(tempPath, path);
1040
- } catch (error) {
1041
- if (file !== undefined) {
1042
- try {
1043
- closeSync(file);
1044
- } catch {}
1045
- }
1046
- try {
1047
- unlinkSync(tempPath);
1048
- } catch (cleanupError) {
1049
- if (errorCode(cleanupError) !== "ENOENT") {
1050
- // Preserve the persistence failure; the best-effort cleanup error is secondary.
1051
- }
1052
- }
1053
- throw error;
1054
- }
1055
- }
1056
-
965
+ /**
966
+ * Applies one compatibility mutation through the shared config writer.
967
+ */
1057
968
  function mutateConfig(
1058
969
  path: string,
1059
970
  mutate: (record: ConfigRecord) => void,
1060
971
  ): PolishedTuiConfig {
1061
- const state = readConfigFileState(path);
1062
- if (state.kind === "corrupt") {
1063
- const detail =
1064
- state.error instanceof Error ? ` (${state.error.message})` : "";
1065
- throw new Error(
1066
- `Refusing to save Zentui config because ${path} is corrupt or unreadable; fix or remove it first.${detail}`,
1067
- );
1068
- }
1069
- mutate(state.record);
1070
- writeConfigAtomically(
1071
- state.writePath,
1072
- state.record,
1073
- state.kind === "valid" ? state.mode : undefined,
1074
- );
1075
- return mergeConfig(state.record);
972
+ const record =
973
+ path === configPath
974
+ ? configStore.update(mutate)
975
+ : mutateConfigFile(path, mutate, "Zentui");
976
+ return mergeConfig(record);
1076
977
  }
1077
978
 
1078
979
  export function ensureConfigExists(_path = configPath): void {
@@ -1807,27 +1708,12 @@ export function getExtensionStatusColorMode(
1807
1708
  return DEFAULT_EXTENSION_STATUS_COLOR_MODE;
1808
1709
  }
1809
1710
 
1711
+ /**
1712
+ * Loads and normalizes the shared raw configuration for shell consumers.
1713
+ */
1810
1714
  export function loadConfig(): PolishedTuiConfig {
1811
1715
  try {
1812
- let raw: string | undefined;
1813
- if (existsSync(configPath)) {
1814
- raw = readFileSync(configPath, "utf8");
1815
- } else if (existsSync(legacyConfigPath)) {
1816
- raw = readFileSync(legacyConfigPath, "utf8");
1817
- try {
1818
- writeFileSync(configPath, raw);
1819
- } catch {
1820
- // Keep using the legacy file in memory if migration cannot be written.
1821
- }
1822
- } else if (existsSync(legacyZentuiConfigPath)) {
1823
- raw = readFileSync(legacyZentuiConfigPath, "utf8");
1824
- try {
1825
- writeFileSync(configPath, raw);
1826
- } catch {
1827
- // Keep using the legacy file in memory if migration cannot be written.
1828
- }
1829
- }
1830
- return mergeConfig(raw ? JSON.parse(raw) : {});
1716
+ return mergeConfig(configStore.read());
1831
1717
  } catch {
1832
1718
  return mergeConfig({});
1833
1719
  }