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.
- package/README.md +66 -39
- package/extensions/{shell → app/commands}/settings-previews.ts +7 -7
- package/extensions/app/config/renderer.ts +13 -63
- package/extensions/app/config/shell.ts +24 -138
- package/extensions/app/config/store.ts +290 -0
- package/extensions/app/host/pi-extension-port.ts +25 -0
- package/extensions/app/host/pi-ui-port.ts +52 -0
- package/extensions/app/host/tui-capabilities.ts +27 -0
- package/extensions/app/overlay/input-router.ts +63 -0
- package/extensions/app/overlay/overlay-manager.ts +39 -0
- package/extensions/{shell → app/overlay}/selector-border.ts +3 -3
- package/extensions/app/overlay/selector-controller.ts +104 -0
- package/extensions/app/ownership/layout-registry.ts +65 -0
- package/extensions/app/ownership.ts +21 -7
- package/extensions/app/panel.ts +261 -140
- package/extensions/app/runtime/event-coordinator.ts +84 -0
- package/extensions/app/runtime/render-scheduler.ts +56 -0
- package/extensions/app/runtime/runtime-state.ts +83 -0
- package/extensions/app/runtime/tui-runtime.ts +546 -0
- package/extensions/features/{context.ts → context-inspector/index.ts} +30 -23
- package/extensions/features/{shell/flush-docked-bash.ts → flush-docked-bash.ts} +1 -4
- package/extensions/index.ts +2 -48
- package/extensions/layouts/context/index.ts +188 -0
- package/extensions/{shell → layouts/context/message}/user-message-styles.ts +2 -2
- package/extensions/{shell → layouts/context/message}/user-message.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/compact-mode.ts +17 -13
- package/extensions/layouts/context/renderer/context-refresh.ts +79 -0
- package/extensions/{transcript → layouts/context}/renderer/default-mode.ts +3 -3
- package/extensions/{transcript → layouts/context}/renderer/index.ts +14 -14
- package/extensions/{transcript → layouts/context}/renderer/mouse/hover.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/mouse/interaction.ts +26 -7
- package/extensions/{transcript → layouts/context}/renderer/mouse/layout.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/mouse/packets.ts +3 -3
- package/extensions/{transcript → layouts/context}/renderer/mouse/scroll.ts +12 -7
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-component.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-highlight.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-layout.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-limits.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-palette.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-presentation.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-renderer.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-write-render.ts +1 -1
- package/extensions/{transcript → layouts/context}/renderer/tool/diff/index.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/grouping.ts +4 -4
- package/extensions/{transcript → layouts/context}/renderer/tool/message-display.ts +6 -4
- package/extensions/{transcript → layouts/context}/renderer/tool/names.ts +2 -2
- package/extensions/{transcript → layouts/context}/renderer/tool/result.ts +3 -3
- package/extensions/{features/agent-summary → layouts/context/summary}/core.ts +1 -1
- package/extensions/{features → layouts/context/thinking}/compact-thinking.ts +7 -7
- package/extensions/{shell → layouts/editor}/accent-rail-editor.ts +8 -8
- package/extensions/{shell → layouts/editor}/completion-menu.ts +3 -6
- package/extensions/layouts/editor/controller.ts +613 -0
- package/extensions/{shell → layouts/editor}/editor-metadata-format.ts +6 -3
- package/extensions/layouts/editor/factory.ts +159 -0
- package/extensions/layouts/editor/index.ts +16 -0
- package/extensions/{shell → layouts/editor}/minimalist-editor.ts +4 -4
- package/extensions/layouts/editor/ownership.ts +103 -0
- package/extensions/{shell → layouts/editor}/ui.ts +2 -2
- package/extensions/layouts/footer/controller.ts +392 -0
- package/extensions/layouts/footer/data.ts +41 -0
- package/extensions/{shell → layouts/footer}/extension-status.ts +2 -2
- package/extensions/{shell → layouts/footer}/footer-layout.ts +1 -1
- package/extensions/{shell → layouts/footer}/footer.ts +11 -8
- package/extensions/layouts/footer/index.ts +24 -0
- package/extensions/layouts/header/index.ts +5 -0
- package/extensions/{features/shell → layouts/header}/startup-header.ts +105 -19
- package/extensions/layouts/working-line/controller.ts +291 -0
- package/extensions/{shell → layouts/working-line}/interaction-summary.ts +3 -3
- package/extensions/{shell → layouts/working-line}/working-line-spinners.ts +1 -1
- package/extensions/{shell → layouts/working-line}/working-line.ts +4 -4
- package/extensions/{shell → services}/live-context.ts +29 -1
- package/extensions/services/project-refresh.ts +374 -0
- package/extensions/{shell → services}/project-state.ts +5 -5
- package/extensions/{shell/state.ts → services/session-state.ts} +75 -5
- package/extensions/{shell → shared}/format.ts +4 -4
- package/extensions/tools/component-tree.ts +6 -6
- package/package.json +4 -4
- package/extensions/shell/index.ts +0 -1659
- package/extensions/shell/project-refresh.ts +0 -130
- package/extensions/shell/settings-command.ts +0 -2144
- package/extensions/transcript/index.ts +0 -65
- package/extensions/transcript/renderer/transcript-refresh.ts +0 -79
- /package/extensions/{shell → app/ownership}/prototype-patch-registry.ts +0 -0
- /package/extensions/{shell → app/runtime}/session-lifecycle.ts +0 -0
- /package/extensions/features/{shell/aliases.ts → aliases.ts} +0 -0
- /package/extensions/features/{shell → legacy}/working-message.ts +0 -0
- /package/extensions/features/{reference → session-reference}/index.ts +0 -0
- /package/extensions/features/{reference → session-reference}/session.ts +0 -0
- /package/extensions/features/{reference/subagent.ts → subagent-autocomplete.ts} +0 -0
- /package/extensions/{shell → layouts/context/message}/user-message-osc.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/markdown-enhance.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ATTRIBUTION.md +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/ansi-utils.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-header.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-inline.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-parse.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/diff-text.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/line-width-safety.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/shiki-highlight.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/diff/write-execution.ts +0 -0
- /package/extensions/{transcript → layouts/context}/renderer/tool/show-more-hint.ts +0 -0
- /package/extensions/{features/agent-summary → layouts/context/summary}/index.ts +0 -0
- /package/extensions/{shell → layouts/editor}/accent-rail-layout-patch.ts +0 -0
- /package/extensions/{shell → layouts/editor}/editor-transfer.ts +0 -0
- /package/extensions/{shell → layouts/footer}/footer-format.ts +0 -0
- /package/extensions/{shell → layouts/working-line}/working-line-messages.ts +0 -0
- /package/extensions/{shell/git.ts → services/git-data.ts} +0 -0
- /package/extensions/{shell/package-version.ts → services/package-data.ts} +0 -0
- /package/extensions/{shell/runtime.ts → services/runtime-data.ts} +0 -0
- /package/extensions/{shell → services}/telemetry.ts +0 -0
- /package/extensions/{shell → shared}/icons.ts +0 -0
- /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 "
|
|
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-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import type {
|
|
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
|
|
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 "
|
|
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 "
|
|
20
|
-
import type { RuntimeInfo } from "
|
|
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
|
|
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
|
-
*
|
|
5
|
-
* grouping
|
|
4
|
+
* The implementation covers the children/getMountedRoots/seen traversal used by
|
|
5
|
+
* context rendering, grouping and mouse packet handling.
|
|
6
6
|
*
|
|
7
|
-
* visitor
|
|
8
|
-
* children
|
|
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
|
-
//
|
|
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.
|
|
4
|
-
"description": "A single, customizable Pi UI package
|
|
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/
|
|
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",
|