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
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
- import type { SessionLifecycle } from "./session-lifecycle.ts";
2
+ import type { SessionLifecycle } from "../app/runtime/session-lifecycle.ts";
3
3
 
4
4
  export type LiveContextOverride = {
5
5
  tokens: number;
@@ -49,10 +49,35 @@ export class LiveContextController {
49
49
  this.requestRender = requestRender;
50
50
  }
51
51
 
52
+ /**
53
+ * Starts a new live-context session and clears predecessor data.
54
+ */
55
+ startSession(): void {
56
+ this.clear();
57
+ }
58
+
59
+ /**
60
+ * Shuts down live-context updates and clears pending redraw state.
61
+ */
62
+ shutdown(): void {
63
+ this.clear();
64
+ }
65
+
66
+ /**
67
+ * Returns the latest valid streaming context override.
68
+ *
69
+ * @returns Current live context override, when available.
70
+ */
52
71
  get(): LiveContextOverride | undefined {
53
72
  return this.override;
54
73
  }
55
74
 
75
+ /**
76
+ * Records a valid assistant usage update and schedules a throttled redraw.
77
+ *
78
+ * @param message Latest assistant message snapshot.
79
+ * @returns Whether the update produced a live context override.
80
+ */
56
81
  update(message: unknown): boolean {
57
82
  const next = liveContextFromMessage(message);
58
83
  if (!next || !this.lifecycle.isCurrent()) return false;
@@ -73,6 +98,9 @@ export class LiveContextController {
73
98
  return true;
74
99
  }
75
100
 
101
+ /**
102
+ * Clears the current override and cancels its pending redraw.
103
+ */
76
104
  clear(): void {
77
105
  this.override = undefined;
78
106
  this.cancelScheduledRender?.();
@@ -0,0 +1,374 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { SessionLifecycle } from "../app/runtime/session-lifecycle.ts";
5
+ import { readGitStatus } from "./git-data.ts";
6
+ import type { FooterState } from "./session-state.ts";
7
+ import { applyProjectRefreshToState } from "./project-state.ts";
8
+ import { readPackageVersionResult } from "./package-data.ts";
9
+ import { readRuntimeInfo } from "./runtime-data.ts";
10
+
11
+ export type StopProjectRefreshInterval = () => void;
12
+
13
+ export type ScheduleProjectRefreshOptions = {
14
+ force?: boolean;
15
+ };
16
+
17
+ export type ProjectRefreshRun = {
18
+ isCurrent: () => boolean;
19
+ };
20
+
21
+ export type ProjectRefreshScheduler<T> = {
22
+ schedule: (target: T, options?: ScheduleProjectRefreshOptions) => void;
23
+ invalidate: () => void;
24
+ stop: () => void;
25
+ };
26
+
27
+ export const PROJECT_REFRESH_THROTTLE_MS = 5_000;
28
+
29
+ export type ProjectRefreshActivationOptions = {
30
+ needed: boolean;
31
+ intervalMs: number;
32
+ onTick: () => void;
33
+ };
34
+
35
+ /**
36
+ * Owns the optional wall-clock interval used by project refresh consumers.
37
+ */
38
+ export class ProjectRefreshActivation {
39
+ private stopInterval: StopProjectRefreshInterval = () => {};
40
+ private active = false;
41
+
42
+ /**
43
+ * Reconciles interval ownership with the current consumer requirements.
44
+ *
45
+ * @param options Current requirement, interval, and refresh callback.
46
+ * @returns Whether the interval was newly activated.
47
+ */
48
+ reconcile(options: ProjectRefreshActivationOptions): boolean {
49
+ if (!options.needed) {
50
+ this.stop();
51
+ return false;
52
+ }
53
+ if (this.active) return false;
54
+ this.stopInterval = startProjectRefreshInterval(
55
+ options.intervalMs,
56
+ options.onTick,
57
+ );
58
+ this.active = true;
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Stops the active interval and clears its ownership state.
64
+ */
65
+ stop(): void {
66
+ this.stopInterval();
67
+ this.stopInterval = () => {};
68
+ this.active = false;
69
+ }
70
+ }
71
+
72
+ export type ProjectRefreshConfig = {
73
+ projectRefreshIntervalMs: number;
74
+ components: {
75
+ footer: {
76
+ styles: {
77
+ starship: {
78
+ gitCommit: { showTag: boolean };
79
+ gitMetrics: { ignoreSubmodules: boolean };
80
+ };
81
+ };
82
+ };
83
+ };
84
+ };
85
+
86
+ export type ProjectRefreshServiceContext = {
87
+ readonly getConfig: () => ProjectRefreshConfig;
88
+ readonly state: FooterState;
89
+ readonly sessionLifecycle: SessionLifecycle;
90
+ readonly getFooterReferences: () => ReadonlySet<string>;
91
+ readonly needsRefresh: () => boolean;
92
+ readonly reconcileOwnership: (ctx: ExtensionContext) => void;
93
+ readonly onProjectRoot: (root: string | undefined) => void;
94
+ readonly refresh: () => void;
95
+ };
96
+
97
+ type ProjectRefreshTarget = { cwd: string; generation: number };
98
+
99
+ /**
100
+ * Coordinates project data reads and interval activation for Footer and Editor.
101
+ */
102
+ export class ProjectRefreshService {
103
+ private readonly context: ProjectRefreshServiceContext;
104
+ private readonly scheduler: ProjectRefreshScheduler<ProjectRefreshTarget>;
105
+ private readonly activation = new ProjectRefreshActivation();
106
+ private lastProjectCwd: string | undefined;
107
+ private requestedProjectCwd: string | undefined;
108
+
109
+ /**
110
+ * Creates a generation-aware project refresh service.
111
+ *
112
+ * @param context Shared state and layout-neutral selectors.
113
+ */
114
+ constructor(context: ProjectRefreshServiceContext) {
115
+ this.context = context;
116
+ this.scheduler = createProjectRefreshScheduler(
117
+ (target, run) => this.refreshProjectState(target, run),
118
+ context.refresh,
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Schedules an immediate or throttled project refresh for a session context.
124
+ *
125
+ * @param ctx Active Pi extension context.
126
+ * @param options Refresh priority options.
127
+ */
128
+ schedule(
129
+ ctx: ExtensionContext,
130
+ options?: ScheduleProjectRefreshOptions,
131
+ ): void {
132
+ const generation = this.context.sessionLifecycle.currentGeneration();
133
+ if (!this.context.sessionLifecycle.isCurrent(generation)) return;
134
+ this.requestedProjectCwd = ctx.cwd;
135
+ this.scheduler.schedule({ cwd: ctx.cwd, generation }, options);
136
+ }
137
+
138
+ /**
139
+ * Reconciles polling ownership with the active Footer or Editor consumers.
140
+ *
141
+ * @param ctx Active Pi extension context.
142
+ * @param force Whether to invalidate an active in-flight schedule.
143
+ */
144
+ reconcile(ctx: ExtensionContext, force = false): void {
145
+ if (
146
+ !this.context.sessionLifecycle.isCurrent() ||
147
+ !this.context.needsRefresh()
148
+ ) {
149
+ this.stop();
150
+ return;
151
+ }
152
+ const activated = this.activation.reconcile({
153
+ needed: true,
154
+ intervalMs: this.context.getConfig().projectRefreshIntervalMs,
155
+ onTick: () => {
156
+ this.context.reconcileOwnership(ctx);
157
+ if (!this.context.needsRefresh()) this.stop();
158
+ else this.schedule(ctx);
159
+ },
160
+ });
161
+ if (force && !activated) this.scheduler.invalidate();
162
+ if (force || activated) this.schedule(ctx, { force: true });
163
+ }
164
+
165
+ /**
166
+ * Stops polling and invalidates all pending project refresh callbacks.
167
+ */
168
+ stop(): void {
169
+ this.activation.stop();
170
+ this.scheduler.stop();
171
+ }
172
+
173
+ /**
174
+ * Refreshes Git, runtime, package, and repository-root data safely.
175
+ *
176
+ * @param target Session cwd and generation target.
177
+ * @param run Scheduler freshness guard.
178
+ */
179
+ private async refreshProjectState(
180
+ { cwd, generation }: ProjectRefreshTarget,
181
+ run: ProjectRefreshRun,
182
+ ): Promise<void> {
183
+ if (
184
+ !run.isCurrent() ||
185
+ !this.context.sessionLifecycle.isCurrent(generation)
186
+ )
187
+ return;
188
+ const starship = this.context.getConfig().components.footer.styles.starship;
189
+ const references = this.context.getFooterReferences();
190
+ const wantExactTag =
191
+ (references.has("git_commit") && starship.gitCommit.showTag) ||
192
+ references.has("git_tag");
193
+ const wantMetrics =
194
+ references.has("git_metrics") ||
195
+ references.has("git_added") ||
196
+ references.has("git_deleted");
197
+ const wantPackage =
198
+ references.has("package") || references.has("package_version");
199
+ const wantRuntime = references.has("runtime");
200
+ const [git, runtime, packageVersion] = await Promise.all([
201
+ readGitStatus(cwd, {
202
+ readExactTag: wantExactTag,
203
+ readMetrics: wantMetrics,
204
+ ignoreSubmodules: starship.gitMetrics.ignoreSubmodules,
205
+ }),
206
+ wantRuntime
207
+ ? readRuntimeInfo(cwd)
208
+ : Promise.resolve({ kind: "ok" as const, runtime: undefined }),
209
+ wantPackage
210
+ ? readPackageVersionResult(cwd)
211
+ : Promise.resolve({ kind: "ok" as const, result: null }),
212
+ ]);
213
+ if (
214
+ !run.isCurrent() ||
215
+ !this.context.sessionLifecycle.isCurrent(generation) ||
216
+ this.requestedProjectCwd !== cwd
217
+ )
218
+ return;
219
+ this.context.onProjectRoot(
220
+ git.kind === "ok" ? findRepositoryRoot(cwd) : undefined,
221
+ );
222
+ this.lastProjectCwd = applyProjectRefreshToState(this.context.state, {
223
+ cwd,
224
+ previousCwd: this.lastProjectCwd,
225
+ git,
226
+ runtime,
227
+ packageVersion,
228
+ });
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Finds the nearest repository root without invoking a shell.
234
+ *
235
+ * @param cwd Starting directory.
236
+ * @returns Repository root, when one is visible.
237
+ */
238
+ function findRepositoryRoot(cwd: string): string | undefined {
239
+ let current = resolve(cwd);
240
+ while (true) {
241
+ if (existsSync(join(current, ".git"))) return current;
242
+ const parent = dirname(current);
243
+ if (parent === current) return undefined;
244
+ current = parent;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Starts a polling interval unless the configured interval disables polling.
250
+ *
251
+ * @param intervalMs Polling interval in milliseconds.
252
+ * @param refresh Callback invoked for each polling tick.
253
+ * @returns Idempotent interval cleanup function.
254
+ */
255
+ export function startProjectRefreshInterval(
256
+ intervalMs: number,
257
+ refresh: () => void,
258
+ ): StopProjectRefreshInterval {
259
+ if (intervalMs <= 0) return () => {};
260
+
261
+ const timer = setInterval(refresh, intervalMs);
262
+ timer.unref?.();
263
+
264
+ return () => clearInterval(timer);
265
+ }
266
+
267
+ /**
268
+ * Creates a throttled, generation-invalidatable project refresh scheduler.
269
+ *
270
+ * @param refresh Asynchronous project refresh operation.
271
+ * @param afterRefresh Callback invoked after a current refresh settles.
272
+ * @param throttleMs Minimum delay between ordinary refreshes.
273
+ * @returns Scheduler controls.
274
+ */
275
+ export function createProjectRefreshScheduler<T>(
276
+ refresh: (target: T, run: ProjectRefreshRun) => Promise<void>,
277
+ afterRefresh: () => void,
278
+ throttleMs = PROJECT_REFRESH_THROTTLE_MS,
279
+ ): ProjectRefreshScheduler<T> {
280
+ let refreshInFlight = false;
281
+ let refreshPending = false;
282
+ let pendingForce = false;
283
+ let pendingTarget: T | undefined;
284
+ let delayedRefresh: ReturnType<typeof setTimeout> | undefined;
285
+ let lastRefreshStartedAt: number | undefined;
286
+ let generation = 0;
287
+
288
+ const clearDelayedRefresh = () => {
289
+ if (!delayedRefresh) return;
290
+ clearTimeout(delayedRefresh);
291
+ delayedRefresh = undefined;
292
+ };
293
+
294
+ const runRefresh = (
295
+ target: T,
296
+ options: ScheduleProjectRefreshOptions = {},
297
+ ) => {
298
+ clearDelayedRefresh();
299
+ if (refreshInFlight) {
300
+ refreshPending = true;
301
+ pendingForce ||= options.force === true;
302
+ pendingTarget = target;
303
+ return;
304
+ }
305
+
306
+ const currentGeneration = generation;
307
+ const run: ProjectRefreshRun = {
308
+ isCurrent: () => currentGeneration === generation,
309
+ };
310
+ refreshInFlight = true;
311
+ lastRefreshStartedAt = Date.now();
312
+ void refresh(target, run)
313
+ .catch(() => undefined)
314
+ .finally(() => {
315
+ if (currentGeneration !== generation) return;
316
+ refreshInFlight = false;
317
+ afterRefresh();
318
+ if (refreshPending) {
319
+ refreshPending = false;
320
+ const nextForce = pendingForce;
321
+ pendingForce = false;
322
+ const nextTarget = pendingTarget ?? target;
323
+ pendingTarget = undefined;
324
+ schedule(nextTarget, { force: nextForce });
325
+ }
326
+ });
327
+ };
328
+
329
+ const schedule = (target: T, options: ScheduleProjectRefreshOptions = {}) => {
330
+ if (
331
+ options.force ||
332
+ throttleMs <= 0 ||
333
+ lastRefreshStartedAt === undefined
334
+ ) {
335
+ runRefresh(target, options);
336
+ return;
337
+ }
338
+
339
+ const delayMs = Math.max(
340
+ 0,
341
+ throttleMs - (Date.now() - lastRefreshStartedAt),
342
+ );
343
+ if (delayMs === 0) {
344
+ runRefresh(target);
345
+ return;
346
+ }
347
+
348
+ pendingTarget = target;
349
+ if (delayedRefresh) return;
350
+ delayedRefresh = setTimeout(() => {
351
+ delayedRefresh = undefined;
352
+ const nextTarget = pendingTarget ?? target;
353
+ pendingTarget = undefined;
354
+ runRefresh(nextTarget);
355
+ }, delayMs);
356
+ delayedRefresh.unref?.();
357
+ };
358
+
359
+ const invalidate = () => {
360
+ generation += 1;
361
+ clearDelayedRefresh();
362
+ refreshInFlight = false;
363
+ refreshPending = false;
364
+ pendingForce = false;
365
+ pendingTarget = undefined;
366
+ lastRefreshStartedAt = undefined;
367
+ };
368
+
369
+ return {
370
+ schedule,
371
+ invalidate,
372
+ stop: invalidate,
373
+ };
374
+ }
@@ -1,8 +1,8 @@
1
- import type { GitReadResult } from "./git.ts";
2
- import { emptyGitStatus } from "./git.ts";
3
- import type { PackageVersionReadResult } from "./package-version.ts";
4
- import type { RuntimeReadResult } from "./runtime.ts";
5
- import type { FooterState } from "./state.ts";
1
+ import type { GitReadResult } from "./git-data.ts";
2
+ import { emptyGitStatus } from "./git-data.ts";
3
+ import type { PackageVersionReadResult } from "./package-data.ts";
4
+ import type { RuntimeReadResult } from "./runtime-data.ts";
5
+ import type { FooterState } from "./session-state.ts";
6
6
 
7
7
  /**
8
8
  * Apply a project refresh (git + runtime) onto footer state, preserving
@@ -8,10 +8,12 @@ import {
8
8
  buildTokenLabel,
9
9
  formatProviderLabel,
10
10
  getUsageTotals,
11
- } from "./format.ts";
12
- import type { GitStatusSummary } from "./git.ts";
13
- import type { PackageVersionResult } from "./package-version.ts";
14
- import type { RuntimeInfo } from "./runtime.ts";
11
+ invalidateUsageTotalsCache,
12
+ } from "../shared/format.ts";
13
+ import { emptyGitStatus } from "./git-data.ts";
14
+ import type { GitStatusSummary } from "./git-data.ts";
15
+ import type { PackageVersionResult } from "./package-data.ts";
16
+ import type { RuntimeInfo } from "./runtime-data.ts";
15
17
  import type { FooterTelemetry } from "./telemetry.ts";
16
18
 
17
19
  export type FooterState = GitStatusSummary & {
@@ -60,6 +62,74 @@ export function modelLabelFor(
60
62
  : state.modelId || "no-model";
61
63
  }
62
64
 
65
+ export type SessionStateSynchronizer = (
66
+ state: FooterState,
67
+ ctx: ExtensionContext,
68
+ cacheHitIcon: string,
69
+ telemetry: FooterTelemetry,
70
+ ) => void;
71
+
72
+ export type SessionStateServiceContext = {
73
+ readonly getCacheHitIcon: () => string;
74
+ readonly resolveTelemetry: (ctx: ExtensionContext) => FooterTelemetry;
75
+ readonly syncState: SessionStateSynchronizer;
76
+ };
77
+
78
+ /**
79
+ * Owns the shared Footer state lifecycle used by the runtime and layouts.
80
+ */
81
+ export class SessionStateService {
82
+ readonly state: FooterState;
83
+ private readonly context: SessionStateServiceContext;
84
+
85
+ /**
86
+ * Creates a session state service with neutral Git defaults.
87
+ *
88
+ * @param context Runtime selectors for icons and optional telemetry.
89
+ */
90
+ constructor(context: SessionStateServiceContext) {
91
+ this.context = context;
92
+ this.state = createInitialState(emptyGitStatus());
93
+ }
94
+
95
+ /**
96
+ * Resets session-scoped state and invalidates cached usage totals.
97
+ */
98
+ startSession(): void {
99
+ this.state.sessionStartEpoch = Date.now();
100
+ invalidateUsageTotalsCache();
101
+ }
102
+
103
+ /**
104
+ * Invalidates cached usage totals before a fresh state synchronization.
105
+ */
106
+ invalidateUsageCache(): void {
107
+ invalidateUsageTotalsCache();
108
+ }
109
+
110
+ /**
111
+ * Synchronizes model, context, usage, cost, and telemetry fields.
112
+ *
113
+ * @param ctx Active Pi extension context.
114
+ */
115
+ sync(ctx: ExtensionContext): void {
116
+ this.context.syncState(
117
+ this.state,
118
+ ctx,
119
+ this.context.getCacheHitIcon(),
120
+ this.context.resolveTelemetry(ctx),
121
+ );
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Synchronizes a Footer state snapshot with the active Pi context.
127
+ *
128
+ * @param state Mutable Footer state snapshot.
129
+ * @param ctx Active Pi extension context.
130
+ * @param cacheHitIcon Cache-hit icon used by the token formatter.
131
+ * @param telemetry Optional telemetry values.
132
+ */
63
133
  export function syncState(
64
134
  state: FooterState,
65
135
  ctx: ExtensionContext,
@@ -70,7 +140,7 @@ export function syncState(
70
140
  const m = ctx.model;
71
141
  state.modelId = m?.id ?? "";
72
142
  state.modelName = m?.name ?? "";
73
- // Retained as a compatibility snapshot only; production surfaces format from raw fields.
143
+ // Retained as a compatibility snapshot only; production layouts format from raw fields.
74
144
  state.modelLabel = modelLabelFor(state, "id");
75
145
  state.providerLabel = formatProviderLabel(ctx.model?.provider);
76
146
  state.contextLabel = buildContextLabel(ctx);
@@ -9,15 +9,15 @@ import type {
9
9
  GitBranchMaxLength,
10
10
  PathDisplayMode,
11
11
  } from "../app/config/shell.ts";
12
- import type { GitCommitInfo, GitMetricsInfo } from "./git.ts";
12
+ import type { GitCommitInfo, GitMetricsInfo } from "../services/git-data.ts";
13
13
  import type { IconMode } from "./icons.ts";
14
14
  import {
15
15
  resolveOsIcon,
16
16
  resolvePackageIcon,
17
17
  resolveRuntimeSymbol,
18
18
  } from "./icons.ts";
19
- import type { PackageVersionResult } from "./package-version.ts";
20
- import type { RuntimeInfo } from "./runtime.ts";
19
+ import type { PackageVersionResult } from "../services/package-data.ts";
20
+ import type { RuntimeInfo } from "../services/runtime-data.ts";
21
21
  import { renderStyleForSource } from "./style.ts";
22
22
 
23
23
  /**
@@ -440,7 +440,7 @@ export function formatRuntimeSegment(
440
440
  /**
441
441
  * Render the package-version segment in Starship `is <glyph> <version>` shape.
442
442
  *
443
- * Distinct from the runtime segment: this surfaces the project's own
443
+ * Distinct from the runtime segment: this layouts the project's own
444
444
  * manifest version (e.g. `package.json#version`), not the installed
445
445
  * toolchain version. Glyph comes from the Starship Nerd Font preset
446
446
  * (https://starship.rs/presets/nerd-font); default color `208` matches
@@ -1,11 +1,11 @@
1
1
  /**
2
- * 组件树递归遍历共享实现。
2
+ * Shared recursive traversal for component trees.
3
3
  *
4
- * 收敛原先散落在 renderer 的 compact-mode / message-display / transcript-refresh /
5
- * grouping / mouse/packets 的「children + getMountedRoots + seen 防环」同款遍历。
4
+ * The implementation covers the children/getMountedRoots/seen traversal used by
5
+ * context rendering, grouping and mouse packet handling.
6
6
  *
7
- * visitor 在每个非数组对象节点上调用一次;返回 `false` 表示不再下钻该节点的
8
- * children 与 mounted roots(对应各处原有的提前 return 语义)。
7
+ * The visitor runs once for every non-array object. Returning `false` stops
8
+ * traversal into that node's children and mounted roots.
9
9
  */
10
10
  export function walkComponentTree(
11
11
  root: any,
@@ -30,7 +30,7 @@ export function walkComponentTree(
30
30
  for (const root of mounted) visit(root);
31
31
  }
32
32
  } catch {
33
- // renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
33
+ // A lazy proxy may temporarily have no mounted roots while switching renderers.
34
34
  }
35
35
  };
36
36
  visit(root);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-one-ui",
3
- "version": "0.1.1",
4
- "description": "A single, customizable Pi UI package combining Zentui shell chrome with Claude Code-style transcript rendering.",
3
+ "version": "0.2.0",
4
+ "description": "A single, customizable Pi UI package with layout-oriented Context and Shell rendering for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -13,11 +13,11 @@
13
13
  "scripts": {
14
14
  "typecheck": "tsc --noEmit",
15
15
  "test": "npm run test:node && npm run test:vitest",
16
- "test:node": "node --test tests/config.test.ts tests/one-ui-integration.test.ts tests/transcript-*.test.ts",
16
+ "test:node": "node --test tests/config.test.ts tests/config-store.test.ts tests/header-*.test.ts tests/one-ui-integration.test.ts tests/overlay-infrastructure.test.ts tests/runtime-infrastructure.test.ts tests/context-*.test.ts",
17
17
  "format": "biome format --write extensions tests package.json tsconfig.json biome.json",
18
18
  "format:check": "biome format extensions tests package.json tsconfig.json biome.json",
19
19
  "lint": "biome check extensions/index.ts extensions/app/panel.ts extensions/app/presets.ts package.json tsconfig.json biome.json",
20
- "test:vitest": "vitest run tests/shell-*.test.ts",
20
+ "test:vitest": "vitest run tests/shell-*.test.ts tests/working-line-*.test.ts tests/editor-*.test.ts tests/footer-*.test.ts tests/services-*.test.ts",
21
21
  "verify": "npm run format:check && npm run lint && npm run typecheck && npm test",
22
22
  "pack:check": "npm pack --dry-run",
23
23
  "pi:dev": "${PI_BIN:-pi} --no-extensions -e ./extensions/index.ts",