pi-cockpit 0.20.0 → 0.22.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/package.json +85 -85
- package/src/agents-store.ts +214 -6
- package/src/config.ts +2 -0
- package/src/index.ts +37 -23
- package/src/public/v1/events.ts +2 -0
- package/src/settings/cockpit-provider.ts +8 -2
- package/src/settings/settings-shell.ts +9 -1
- package/src/types.ts +3 -0
- package/src/viewport-stability.ts +12 -21
package/package.json
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"pi-package",
|
|
8
|
-
"pi",
|
|
9
|
-
"pi-extension",
|
|
10
|
-
"tui",
|
|
11
|
-
"teammate",
|
|
12
|
-
"todo"
|
|
13
|
-
],
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"files": [
|
|
16
|
-
"src/",
|
|
17
|
-
"themes/",
|
|
18
|
-
"README.md"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
|
|
22
|
-
"typecheck": "tsc --noEmit",
|
|
23
|
-
"prepack": "node ../../scripts/bundle-settings-core.mjs",
|
|
24
|
-
"postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
|
|
25
|
-
},
|
|
26
|
-
"pi": {
|
|
27
|
-
"extensions": [
|
|
28
|
-
"./src/extension/index.ts"
|
|
29
|
-
],
|
|
30
|
-
"themes": [
|
|
31
|
-
"./themes"
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"pi-maestro-settings-core": "0.2.1",
|
|
36
|
-
"proper-lockfile": "4.1.2"
|
|
37
|
-
},
|
|
38
|
-
"bundledDependencies": [
|
|
39
|
-
"pi-maestro-settings-core"
|
|
40
|
-
],
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-agent-core": "*",
|
|
43
|
-
"@earendil-works/pi-ai": "*",
|
|
44
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
-
"@earendil-works/pi-tui": "*",
|
|
46
|
-
"pi-maestro-teammate": "^2.
|
|
47
|
-
"typebox": "*"
|
|
48
|
-
},
|
|
49
|
-
"peerDependenciesMeta": {
|
|
50
|
-
"@earendil-works/pi-agent-core": {
|
|
51
|
-
"optional": true
|
|
52
|
-
},
|
|
53
|
-
"@earendil-works/pi-ai": {
|
|
54
|
-
"optional": true
|
|
55
|
-
},
|
|
56
|
-
"@earendil-works/pi-coding-agent": {
|
|
57
|
-
"optional": true
|
|
58
|
-
},
|
|
59
|
-
"@earendil-works/pi-tui": {
|
|
60
|
-
"optional": true
|
|
61
|
-
},
|
|
62
|
-
"pi-maestro-teammate": {
|
|
63
|
-
"optional": true
|
|
64
|
-
},
|
|
65
|
-
"typebox": {
|
|
66
|
-
"optional": true
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
"devDependencies": {
|
|
70
|
-
"@earendil-works/pi-agent-core": "0.84.4",
|
|
71
|
-
"@earendil-works/pi-ai": "0.84.4",
|
|
72
|
-
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
|
-
"@earendil-works/pi-tui": "0.84.4",
|
|
74
|
-
"@types/node": "^22.0.0",
|
|
75
|
-
"pi-maestro-teammate": "2.
|
|
76
|
-
"typebox": "1.3.7",
|
|
77
|
-
"typescript": "^5.5.0"
|
|
78
|
-
},
|
|
79
|
-
"main": "./src/index.ts",
|
|
80
|
-
"exports": {
|
|
81
|
-
".": "./src/index.ts",
|
|
82
|
-
"./v1/events": "./src/public/v1/events.ts",
|
|
83
|
-
"./src/*": "./src/*"
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"tui",
|
|
11
|
+
"teammate",
|
|
12
|
+
"todo"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"themes/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.test.ts tests/quiet-tools.test.ts tests/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/model-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepack": "node ../../scripts/bundle-settings-core.mjs",
|
|
24
|
+
"postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./src/extension/index.ts"
|
|
29
|
+
],
|
|
30
|
+
"themes": [
|
|
31
|
+
"./themes"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"pi-maestro-settings-core": "0.2.1",
|
|
36
|
+
"proper-lockfile": "4.1.2"
|
|
37
|
+
},
|
|
38
|
+
"bundledDependencies": [
|
|
39
|
+
"pi-maestro-settings-core"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-agent-core": "*",
|
|
43
|
+
"@earendil-works/pi-ai": "*",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
+
"@earendil-works/pi-tui": "*",
|
|
46
|
+
"pi-maestro-teammate": "^2.5.0",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@earendil-works/pi-agent-core": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-ai": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@earendil-works/pi-coding-agent": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@earendil-works/pi-tui": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"pi-maestro-teammate": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"typebox": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@earendil-works/pi-agent-core": "0.84.4",
|
|
71
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"pi-maestro-teammate": "2.5.0",
|
|
76
|
+
"typebox": "1.3.7",
|
|
77
|
+
"typescript": "^5.5.0"
|
|
78
|
+
},
|
|
79
|
+
"main": "./src/index.ts",
|
|
80
|
+
"exports": {
|
|
81
|
+
".": "./src/index.ts",
|
|
82
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
83
|
+
"./src/*": "./src/*"
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/agents-store.ts
CHANGED
|
@@ -11,19 +11,227 @@ import type {
|
|
|
11
11
|
} from "pi-maestro-teammate/v1/types";
|
|
12
12
|
import type {
|
|
13
13
|
RuntimeAgentReadEntityV2,
|
|
14
|
+
RuntimeReadModelChangeV2,
|
|
14
15
|
RuntimeReadModelDeltaV2,
|
|
15
16
|
RuntimeReadModelOwnershipV2,
|
|
16
17
|
RuntimeReadModelSnapshotV2,
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
RuntimeReadModelProjectionV2,
|
|
20
|
-
parseRuntimeReadModelDeltaV2,
|
|
21
|
-
parseRuntimeReadModelOwnershipV2,
|
|
22
|
-
parseRuntimeReadModelSnapshotV2,
|
|
18
|
+
RuntimeReadModelSourceV2,
|
|
23
19
|
} from "pi-maestro-teammate/v2/runtime";
|
|
24
20
|
import { sanitizeExtensionStatusText } from "./extension-status.ts";
|
|
25
21
|
import { EXPERT_LEADER_NAME, type AgentRow, type AgentStatus } from "./types.ts";
|
|
26
22
|
|
|
23
|
+
export const RUNTIME_READ_MODEL_DELTA_EVENT = "teammate:runtime-read-model-delta-v2";
|
|
24
|
+
export const RUNTIME_READ_MODEL_QUERY_EVENT = "teammate:runtime-read-model-query-v2";
|
|
25
|
+
export const RUNTIME_READ_MODEL_SNAPSHOT_EVENT = "teammate:runtime-read-model-snapshot-v2";
|
|
26
|
+
export const RUNTIME_READ_MODEL_UNAVAILABLE_EVENT = "teammate:runtime-read-model-unavailable-v2";
|
|
27
|
+
|
|
28
|
+
/** Keep Cockpit loadable when its optional Teammate peer is absent. */
|
|
29
|
+
export function runtimeV2ReadEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
30
|
+
return env.PI_RUNTIME_BROKER?.trim().toLowerCase() === "sqlite"
|
|
31
|
+
&& (env.PI_RUNTIME_V2_READ === undefined || env.PI_RUNTIME_V2_READ === "1");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function plainObject(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeInteger(value: unknown, minimum = 0): value is number {
|
|
39
|
+
return Number.isSafeInteger(value) && (value as number) >= minimum;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function identifier(value: unknown): value is string {
|
|
43
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseRuntimeReadModelOwnershipV2(value: unknown): RuntimeReadModelOwnershipV2 | undefined {
|
|
47
|
+
if (!plainObject(value)
|
|
48
|
+
|| !identifier(value.workspaceId)
|
|
49
|
+
|| !identifier(value.sessionId)
|
|
50
|
+
|| !identifier(value.sourceId)
|
|
51
|
+
|| !safeInteger(value.generation, 1)) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
workspaceId: value.workspaceId,
|
|
54
|
+
sessionId: value.sessionId,
|
|
55
|
+
sourceId: value.sourceId,
|
|
56
|
+
generation: value.generation,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseSource(value: unknown): RuntimeReadModelSourceV2 | undefined {
|
|
61
|
+
if (!plainObject(value)
|
|
62
|
+
|| !identifier(value.streamId)
|
|
63
|
+
|| !safeInteger(value.revision, 1)
|
|
64
|
+
|| !safeInteger(value.generation, 1)) return undefined;
|
|
65
|
+
const projection = value.projection === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: parseRuntimeReadModelOwnershipV2(value.projection);
|
|
68
|
+
if (value.projection !== undefined && !projection) return undefined;
|
|
69
|
+
return {
|
|
70
|
+
streamId: value.streamId,
|
|
71
|
+
revision: value.revision,
|
|
72
|
+
generation: value.generation,
|
|
73
|
+
...(projection ? { projection } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const AGENT_STATUSES = new Set<RuntimeAgentReadEntityV2["status"]>([
|
|
78
|
+
"pending", "running", "retrying", "sleeping", "completed", "failed", "terminated",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
function parseRuntimeEntity(value: unknown): RuntimeAgentReadEntityV2 | undefined {
|
|
82
|
+
if (!plainObject(value)
|
|
83
|
+
|| !identifier(value.correlationId)
|
|
84
|
+
|| !safeInteger(value.generation, 1)
|
|
85
|
+
|| !identifier(value.agent)
|
|
86
|
+
|| !AGENT_STATUSES.has(value.status as RuntimeAgentReadEntityV2["status"])
|
|
87
|
+
|| !safeInteger(value.startedAt)
|
|
88
|
+
|| !safeInteger(value.lastActivityAt)) return undefined;
|
|
89
|
+
for (const key of ["name", "task", "spawnedBy", "parentCorrelationId", "phase", "requestedModel", "resolvedModel", "lastMessage", "error"] as const) {
|
|
90
|
+
if (value[key] !== undefined && typeof value[key] !== "string") return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (value.dependencies !== undefined
|
|
93
|
+
&& (!Array.isArray(value.dependencies) || value.dependencies.some((item) => !safeInteger(item)))) return undefined;
|
|
94
|
+
if (value.attemptedModels !== undefined
|
|
95
|
+
&& (!Array.isArray(value.attemptedModels) || value.attemptedModels.some((item) => typeof item !== "string"))) return undefined;
|
|
96
|
+
if (value.projection !== undefined && !parseRuntimeReadModelOwnershipV2(value.projection)) return undefined;
|
|
97
|
+
return structuredClone(value) as unknown as RuntimeAgentReadEntityV2;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseRuntimeReadModelDeltaV2(value: unknown): RuntimeReadModelDeltaV2 | undefined {
|
|
101
|
+
if (!plainObject(value)
|
|
102
|
+
|| value.version !== 2
|
|
103
|
+
|| value.revision !== 1
|
|
104
|
+
|| value.kind !== "agent-runs-delta"
|
|
105
|
+
|| !safeInteger(value.baseCursor)
|
|
106
|
+
|| !safeInteger(value.nextCursor, 1)
|
|
107
|
+
|| value.nextCursor <= value.baseCursor
|
|
108
|
+
|| !Array.isArray(value.changes)) return undefined;
|
|
109
|
+
const source = parseSource(value.source);
|
|
110
|
+
if (!source) return undefined;
|
|
111
|
+
const changes: RuntimeReadModelChangeV2[] = [];
|
|
112
|
+
for (const change of value.changes) {
|
|
113
|
+
if (!plainObject(change)) return undefined;
|
|
114
|
+
if (change.kind === "upsert") {
|
|
115
|
+
const entity = parseRuntimeEntity(change.entity);
|
|
116
|
+
if (!entity) return undefined;
|
|
117
|
+
changes.push({ kind: "upsert", entity });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (change.kind === "tombstone"
|
|
121
|
+
&& identifier(change.correlationId)
|
|
122
|
+
&& safeInteger(change.generation, 1)) {
|
|
123
|
+
changes.push({ kind: "tombstone", correlationId: change.correlationId, generation: change.generation });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
version: 2,
|
|
130
|
+
revision: 1,
|
|
131
|
+
kind: "agent-runs-delta",
|
|
132
|
+
baseCursor: value.baseCursor,
|
|
133
|
+
nextCursor: value.nextCursor,
|
|
134
|
+
source,
|
|
135
|
+
changes,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseRuntimeReadModelSnapshotV2(value: unknown): RuntimeReadModelSnapshotV2 | undefined {
|
|
140
|
+
if (!plainObject(value)
|
|
141
|
+
|| value.version !== 2
|
|
142
|
+
|| value.revision !== 1
|
|
143
|
+
|| value.kind !== "agent-runs-snapshot"
|
|
144
|
+
|| !safeInteger(value.cursor)
|
|
145
|
+
|| !Array.isArray(value.agents)) return undefined;
|
|
146
|
+
const source = parseSource(value.source);
|
|
147
|
+
if (!source) return undefined;
|
|
148
|
+
const agents = value.agents.map(parseRuntimeEntity);
|
|
149
|
+
if (agents.some((agent) => agent === undefined)) return undefined;
|
|
150
|
+
return {
|
|
151
|
+
version: 2,
|
|
152
|
+
revision: 1,
|
|
153
|
+
kind: "agent-runs-snapshot",
|
|
154
|
+
cursor: value.cursor,
|
|
155
|
+
source,
|
|
156
|
+
agents: agents as RuntimeAgentReadEntityV2[],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function cloneRuntimeEntity(entity: RuntimeAgentReadEntityV2): RuntimeAgentReadEntityV2 {
|
|
161
|
+
return structuredClone(entity);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class RuntimeReadModelProjectionV2 {
|
|
165
|
+
readonly #agents = new Map<string, RuntimeAgentReadEntityV2>();
|
|
166
|
+
#cursor = 0;
|
|
167
|
+
#source: RuntimeReadModelSourceV2 = { streamId: "uninitialized", revision: 1, generation: 1 };
|
|
168
|
+
#initialized = false;
|
|
169
|
+
|
|
170
|
+
get cursor(): number { return this.#cursor; }
|
|
171
|
+
|
|
172
|
+
applySnapshot(input: unknown): boolean {
|
|
173
|
+
const snapshot = parseRuntimeReadModelSnapshotV2(input);
|
|
174
|
+
if (!snapshot) return false;
|
|
175
|
+
if (this.#initialized && snapshot.source.streamId === this.#source.streamId) {
|
|
176
|
+
if (snapshot.source.generation < this.#source.generation) return false;
|
|
177
|
+
if (snapshot.source.generation === this.#source.generation && snapshot.cursor < this.#cursor) return false;
|
|
178
|
+
}
|
|
179
|
+
const next = new Map<string, RuntimeAgentReadEntityV2>();
|
|
180
|
+
for (const agent of snapshot.agents) {
|
|
181
|
+
const existing = next.get(agent.correlationId);
|
|
182
|
+
if (existing && existing.generation > agent.generation) continue;
|
|
183
|
+
next.set(agent.correlationId, cloneRuntimeEntity(agent));
|
|
184
|
+
}
|
|
185
|
+
this.#agents.clear();
|
|
186
|
+
for (const [id, agent] of next) this.#agents.set(id, agent);
|
|
187
|
+
this.#cursor = snapshot.cursor;
|
|
188
|
+
this.#source = { ...snapshot.source };
|
|
189
|
+
this.#initialized = true;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
applyDelta(input: unknown): boolean {
|
|
194
|
+
const delta = parseRuntimeReadModelDeltaV2(input);
|
|
195
|
+
if (!delta
|
|
196
|
+
|| delta.baseCursor !== this.#cursor
|
|
197
|
+
|| delta.nextCursor <= this.#cursor
|
|
198
|
+
|| delta.source.streamId !== this.#source.streamId
|
|
199
|
+
|| delta.source.generation !== this.#source.generation
|
|
200
|
+
|| JSON.stringify(delta.source.projection) !== JSON.stringify(this.#source.projection)
|
|
201
|
+
|| delta.source.revision !== delta.nextCursor) return false;
|
|
202
|
+
const next = new Map([...this.#agents].map(([id, agent]) => [id, cloneRuntimeEntity(agent)]));
|
|
203
|
+
for (const change of delta.changes) {
|
|
204
|
+
if (change.kind === "upsert") {
|
|
205
|
+
const current = next.get(change.entity.correlationId);
|
|
206
|
+
if (current && change.entity.generation < current.generation) return false;
|
|
207
|
+
next.set(change.entity.correlationId, cloneRuntimeEntity(change.entity));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const current = next.get(change.correlationId);
|
|
211
|
+
if (current && change.generation < current.generation) return false;
|
|
212
|
+
if (!current || change.generation === current.generation) next.delete(change.correlationId);
|
|
213
|
+
}
|
|
214
|
+
this.#agents.clear();
|
|
215
|
+
for (const [id, agent] of next) this.#agents.set(id, agent);
|
|
216
|
+
this.#cursor = delta.nextCursor;
|
|
217
|
+
this.#source = { ...delta.source };
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
snapshot(): RuntimeReadModelSnapshotV2 {
|
|
222
|
+
return {
|
|
223
|
+
version: 2,
|
|
224
|
+
revision: 1,
|
|
225
|
+
kind: "agent-runs-snapshot",
|
|
226
|
+
cursor: this.#cursor,
|
|
227
|
+
source: { ...this.#source },
|
|
228
|
+
agents: [...this.#agents.values()]
|
|
229
|
+
.sort((left, right) => left.correlationId.localeCompare(right.correlationId))
|
|
230
|
+
.map(cloneRuntimeEntity),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
27
235
|
const STATUS_TEXT_MAX = 48;
|
|
28
236
|
/** Selected-session output kept for the expandable fixed detail region. */
|
|
29
237
|
export const SESSION_CONTENT_MAX = 2_048;
|
package/src/config.ts
CHANGED
|
@@ -76,6 +76,7 @@ const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "
|
|
|
76
76
|
agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
|
|
77
77
|
todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
|
|
78
78
|
todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
|
|
79
|
+
todoDurationChart: typeof o.todoDurationChart === "boolean" ? o.todoDurationChart : base.todoDurationChart,
|
|
79
80
|
stackStyle: isStackStyle(o.stackStyle) ? o.stackStyle : base.stackStyle,
|
|
80
81
|
hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
|
|
81
82
|
pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
|
|
@@ -168,6 +169,7 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
|
|
|
168
169
|
agentsMode: config.agentsMode,
|
|
169
170
|
todoMode: config.todoMode,
|
|
170
171
|
todoExpanded: config.todoExpanded,
|
|
172
|
+
todoDurationChart: config.todoDurationChart,
|
|
171
173
|
stackStyle: config.stackStyle,
|
|
172
174
|
hideNativeAgents: config.hideNativeAgents,
|
|
173
175
|
pinEditorBottom: config.pinEditorBottom,
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,18 @@ import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext, type Exte
|
|
|
4
4
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import { ambientKeysShouldYield, capturingOverlayVisible } from "./capturing-overlay.ts";
|
|
6
6
|
import { Key, decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AgentReadStoreRouter,
|
|
9
|
+
RUNTIME_READ_MODEL_DELTA_EVENT,
|
|
10
|
+
RUNTIME_READ_MODEL_QUERY_EVENT,
|
|
11
|
+
RUNTIME_READ_MODEL_SNAPSHOT_EVENT,
|
|
12
|
+
RUNTIME_READ_MODEL_UNAVAILABLE_EVENT,
|
|
13
|
+
effectiveAgentStatus,
|
|
14
|
+
runtimeV2ReadEnabled,
|
|
15
|
+
type CompletePayload,
|
|
16
|
+
type MessagePayload,
|
|
17
|
+
type StartedPayload,
|
|
18
|
+
} from "./agents-store.ts";
|
|
8
19
|
import { AmbientSurfaceCache, nextUiPromptDepth, shouldHideWorkingDuration, statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
9
20
|
import { generateTitleWithModel } from "./title-llm.ts";
|
|
10
21
|
import { suggestTitle } from "./title-gen.ts";
|
|
@@ -120,13 +131,6 @@ import {
|
|
|
120
131
|
type CockpitConfig,
|
|
121
132
|
} from "./types.ts";
|
|
122
133
|
import type { MailboxHostRegistry } from "pi-maestro-teammate/v1/mailbox";
|
|
123
|
-
import {
|
|
124
|
-
RUNTIME_READ_MODEL_DELTA_EVENT,
|
|
125
|
-
RUNTIME_READ_MODEL_QUERY_EVENT,
|
|
126
|
-
RUNTIME_READ_MODEL_SNAPSHOT_EVENT,
|
|
127
|
-
RUNTIME_READ_MODEL_UNAVAILABLE_EVENT,
|
|
128
|
-
runtimeV2ReadEnabled,
|
|
129
|
-
} from "pi-maestro-teammate/v2/runtime";
|
|
130
134
|
|
|
131
135
|
export {
|
|
132
136
|
EndpointStore,
|
|
@@ -357,6 +361,27 @@ export default function (pi: ExtensionAPI): void {
|
|
|
357
361
|
// thinking label / teammate tree still stream above the visible viewport.
|
|
358
362
|
let stabilityTui: TUI | undefined;
|
|
359
363
|
let viewportStabilityPatch: ViewportStabilityPatch | undefined;
|
|
364
|
+
const ensureViewportStability = (tui: TUI): void => {
|
|
365
|
+
// Native fullscreen owns a fixed application viewport and has no main-screen
|
|
366
|
+
// applyLineResets hook. Do not cache that expected miss: the same dynamic TUI
|
|
367
|
+
// reference can later dispatch to the regular renderer.
|
|
368
|
+
if (tui.mode === "fullscreen") return;
|
|
369
|
+
if (stabilityTui === tui && viewportStabilityPatch?.active) return;
|
|
370
|
+
viewportStabilityPatch?.detach();
|
|
371
|
+
const patch = attachViewportStability(tui);
|
|
372
|
+
viewportStabilityPatch = patch;
|
|
373
|
+
stabilityTui = patch.active ? tui : undefined;
|
|
374
|
+
};
|
|
375
|
+
const requestCapturedRender = (): void => {
|
|
376
|
+
try {
|
|
377
|
+
const tui = capturedTui;
|
|
378
|
+
if (!tui) return;
|
|
379
|
+
ensureViewportStability(tui);
|
|
380
|
+
tui.requestRender();
|
|
381
|
+
} catch {
|
|
382
|
+
// tui may be gone between sessions
|
|
383
|
+
}
|
|
384
|
+
};
|
|
360
385
|
// True while Cockpit owns a capturing overlay (bash jobs, theme picker,
|
|
361
386
|
// settings panel). Split-pane resize must yield to the overlay's focus and
|
|
362
387
|
// refuse to start while one is open; otherwise the resize listener — a
|
|
@@ -455,13 +480,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
455
480
|
// while the model thinks, then the settled duration. Pi owns the fold.
|
|
456
481
|
const thinkingTimer = new ThinkingFoldTimer({
|
|
457
482
|
getTui: () => capturedTui,
|
|
458
|
-
requestRender:
|
|
459
|
-
try {
|
|
460
|
-
capturedTui?.requestRender();
|
|
461
|
-
} catch {
|
|
462
|
-
// tui may be gone between sessions
|
|
463
|
-
}
|
|
464
|
-
},
|
|
483
|
+
requestRender: requestCapturedRender,
|
|
465
484
|
getBaseLabel: () => (config.quietMode ? quietThinkingLabel() : undefined),
|
|
466
485
|
getGlyphs: () => resolveGlyphs(config.icons.mode),
|
|
467
486
|
isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
|
|
@@ -601,7 +620,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
601
620
|
if (refreshAmbientSurface) refreshAmbient(now);
|
|
602
621
|
if (agentPriorityChanged) publishUiOwnership();
|
|
603
622
|
try {
|
|
604
|
-
|
|
623
|
+
requestCapturedRender();
|
|
605
624
|
activeAgentOverlayRender?.();
|
|
606
625
|
sidebarController?.requestRender();
|
|
607
626
|
} catch {
|
|
@@ -691,6 +710,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
691
710
|
sidebar: ownsDock,
|
|
692
711
|
goal: ownsDock,
|
|
693
712
|
todoExpanded: effectiveTodoExpanded(),
|
|
713
|
+
todoDurationChart: config.todoDurationChart,
|
|
694
714
|
quiet: config.enabled && config.quietMode,
|
|
695
715
|
quietSymbols: config.quietSymbols,
|
|
696
716
|
static: config.staticMode,
|
|
@@ -747,13 +767,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
747
767
|
};
|
|
748
768
|
|
|
749
769
|
|
|
750
|
-
const ensureViewportStability = (tui: TUI): void => {
|
|
751
|
-
if (stabilityTui === tui) return;
|
|
752
|
-
viewportStabilityPatch?.detach();
|
|
753
|
-
viewportStabilityPatch = attachViewportStability(tui);
|
|
754
|
-
stabilityTui = tui;
|
|
755
|
-
};
|
|
756
|
-
|
|
757
770
|
const sessionListOverlayActive = (): boolean =>
|
|
758
771
|
capturingOverlayActive || ambientKeysShouldYield(capturedTui);
|
|
759
772
|
|
|
@@ -1916,6 +1929,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1916
1929
|
sidebar: false,
|
|
1917
1930
|
goal: false,
|
|
1918
1931
|
todoExpanded: config.todoExpanded,
|
|
1932
|
+
todoDurationChart: config.todoDurationChart,
|
|
1919
1933
|
quiet: false,
|
|
1920
1934
|
quietSymbols: config.quietSymbols,
|
|
1921
1935
|
static: config.staticMode,
|
package/src/public/v1/events.ts
CHANGED
|
@@ -195,6 +195,8 @@ export interface CockpitUiOwnershipV1 {
|
|
|
195
195
|
/** True only while the dock renders Maestro Goal state. */
|
|
196
196
|
goal: boolean;
|
|
197
197
|
todoExpanded: boolean;
|
|
198
|
+
/** Whether Flow should render the duration chart after a terminal todo advance. */
|
|
199
|
+
todoDurationChart?: boolean;
|
|
198
200
|
quiet: boolean;
|
|
199
201
|
quietSymbols: "check" | "dot";
|
|
200
202
|
/** Static mode: per-second churn (elapsed ticks, spinners) is suppressed. */
|
|
@@ -68,6 +68,7 @@ const CONFIG_KEYS = [
|
|
|
68
68
|
"agentsMode",
|
|
69
69
|
"todoMode",
|
|
70
70
|
"todoExpanded",
|
|
71
|
+
"todoDurationChart",
|
|
71
72
|
"stackStyle",
|
|
72
73
|
"hideNativeAgents",
|
|
73
74
|
"toolPalette",
|
|
@@ -164,6 +165,8 @@ const CATALOGS = {
|
|
|
164
165
|
"cockpit.agentsMode": "Agent display",
|
|
165
166
|
"cockpit.todoMode": "Todo display",
|
|
166
167
|
"cockpit.todoExpanded": "Todo expanded",
|
|
168
|
+
"cockpit.todoDurationChart": "Advance duration chart",
|
|
169
|
+
"cockpit.todoDurationChart.description": "Show a task-duration chart after advance completes the entire Todo list.",
|
|
167
170
|
"cockpit.stackStyle": "Stack projection",
|
|
168
171
|
"cockpit.hideNativeAgents": "Hide native agent widget",
|
|
169
172
|
"cockpit.icons.mode": "Icon mode",
|
|
@@ -249,6 +252,8 @@ const CATALOGS = {
|
|
|
249
252
|
"cockpit.agentsMode": "Agent 显示",
|
|
250
253
|
"cockpit.todoMode": "Todo 显示",
|
|
251
254
|
"cockpit.todoExpanded": "展开 Todo",
|
|
255
|
+
"cockpit.todoDurationChart": "Advance 耗时图",
|
|
256
|
+
"cockpit.todoDurationChart.description": "当 advance 完成整个 Todo 列表后显示任务耗时统计图。",
|
|
252
257
|
"cockpit.stackStyle": "栈投影",
|
|
253
258
|
"cockpit.hideNativeAgents": "隐藏原生 Agent 面板",
|
|
254
259
|
"cockpit.icons.mode": "图标模式",
|
|
@@ -367,8 +372,9 @@ const DEFINITIONS: readonly SettingDefinition[] = [
|
|
|
367
372
|
enumDefinition("agentsMode", "cockpit.group.panels", 0, "cockpit.agentsMode", ["list", "compact"], "live"),
|
|
368
373
|
enumDefinition("todoMode", "cockpit.group.panels", 1, "cockpit.todoMode", ["list", "compact"], "live"),
|
|
369
374
|
booleanDefinition("todoExpanded", "cockpit.group.panels", 2, "cockpit.todoExpanded", "live"),
|
|
375
|
+
booleanDefinition("todoDurationChart", "cockpit.group.panels", 3, "cockpit.todoDurationChart", "live", "cockpit.todoDurationChart.description"),
|
|
370
376
|
enumDefinition("stackStyle", "cockpit.group.layout", 4, "cockpit.stackStyle", ["classic", "zen"], "live"),
|
|
371
|
-
booleanDefinition("hideNativeAgents", "cockpit.group.panels",
|
|
377
|
+
booleanDefinition("hideNativeAgents", "cockpit.group.panels", 4, "cockpit.hideNativeAgents", "live"),
|
|
372
378
|
enumDefinition("sidebar.mode", "cockpit.group.sidebar", 0, "cockpit.sidebar.mode", ["auto", "on", "off"], "live"),
|
|
373
379
|
{
|
|
374
380
|
key: "sidebar.width",
|
|
@@ -789,7 +795,7 @@ function setUsageValue(usage: CockpitConfig["usage"], field: string, value: Json
|
|
|
789
795
|
}
|
|
790
796
|
|
|
791
797
|
function validValue(key: CockpitSettingKey, value: JsonValue): boolean {
|
|
792
|
-
if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
|
|
798
|
+
if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "todoDurationChart", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
|
|
793
799
|
if (key === "sidebar.width") return typeof value === "number" && Number.isSafeInteger(value) && value >= 32 && value <= 56;
|
|
794
800
|
if (key === "title.maxLength") return typeof value === "number" && Number.isSafeInteger(value) && value >= 20 && value <= 200;
|
|
795
801
|
if (key === "title.generationModel") return typeof value === "string";
|
|
@@ -1283,7 +1283,15 @@ export class MaestroSettingsShell implements Component, Focusable {
|
|
|
1283
1283
|
const change = this.params.coordinator.changes(providerId).find((entry) => entry.key === definition.key && entry.scope === scope);
|
|
1284
1284
|
if (change?.operation === "unset") return this.t("settings.inherited");
|
|
1285
1285
|
if (change?.operation === "set") return this.formatValue(definition, change.value);
|
|
1286
|
-
|
|
1286
|
+
const baseline = this.params.coordinator.baseline(providerId);
|
|
1287
|
+
const configured = this.configuredValue(baseline, definition.key, scope);
|
|
1288
|
+
if (!configured || configured.state === "absent") {
|
|
1289
|
+
const effective = baseline?.effective.values.find((value) => value.key === definition.key);
|
|
1290
|
+
if (effective) {
|
|
1291
|
+
return `${this.t("settings.state.absent")} · ${this.t("settings.effective")} ${this.formatValue(definition, effective.value)}`;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return this.displayConfigured(definition, configured);
|
|
1287
1295
|
}
|
|
1288
1296
|
|
|
1289
1297
|
private configuredValue(snapshot: SettingsSnapshot | undefined, key: string, scope: SettingsScope) {
|
package/src/types.ts
CHANGED
|
@@ -260,6 +260,8 @@ export interface CockpitConfig {
|
|
|
260
260
|
agentsMode: ViewMode;
|
|
261
261
|
todoMode: ViewMode;
|
|
262
262
|
todoExpanded: boolean;
|
|
263
|
+
/** Show the duration chart after a terminal todo advance. */
|
|
264
|
+
todoDurationChart: boolean;
|
|
263
265
|
/** Above-editor stack projection: classic Todo/Agents widgets or the unified Zen stack. */
|
|
264
266
|
stackStyle: StackStyle;
|
|
265
267
|
hideNativeAgents: boolean;
|
|
@@ -318,6 +320,7 @@ export const DEFAULT_CONFIG: CockpitConfig = {
|
|
|
318
320
|
agentsMode: "list",
|
|
319
321
|
todoMode: "list",
|
|
320
322
|
todoExpanded: false,
|
|
323
|
+
todoDurationChart: true,
|
|
321
324
|
stackStyle: "classic",
|
|
322
325
|
hideNativeAgents: true,
|
|
323
326
|
pinEditorBottom: false,
|
|
@@ -10,7 +10,8 @@ interface ViewportTuiInternals {
|
|
|
10
10
|
previousLines?: string[];
|
|
11
11
|
previousViewportTop?: number;
|
|
12
12
|
previousHeight?: number;
|
|
13
|
-
|
|
13
|
+
previousWidth?: number;
|
|
14
|
+
terminal?: { columns: number; rows: number };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
interface ViewportStabilityMarker {
|
|
@@ -44,18 +45,6 @@ function markerOf(fn: ApplyLineResets): ViewportStabilityMarker | undefined {
|
|
|
44
45
|
return (fn as ApplyLineResets & Record<symbol, ViewportStabilityMarker | undefined>)[VIEWPORT_STABILITY_MARKER];
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
|
|
48
|
-
const TRAILING_DURATION = /(\s(?:·\s*)?)(?:\d+(?:\.\d+)?s|\d+m\d{1,2}s)$/;
|
|
49
|
-
|
|
50
|
-
function differsOnlyByTrailingDuration(previous: string, next: string): boolean {
|
|
51
|
-
const previousPlain = previous.replace(ANSI_ESCAPE, "");
|
|
52
|
-
const nextPlain = next.replace(ANSI_ESCAPE, "");
|
|
53
|
-
if (previousPlain === nextPlain) return false;
|
|
54
|
-
if (!TRAILING_DURATION.test(previousPlain) || !TRAILING_DURATION.test(nextPlain)) return false;
|
|
55
|
-
return previousPlain.replace(TRAILING_DURATION, "$1<duration>")
|
|
56
|
-
=== nextPlain.replace(TRAILING_DURATION, "$1<duration>");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
48
|
function isKittyImageLine(line: string | undefined): boolean {
|
|
60
49
|
return typeof line === "string" && line.includes("\x1b_G");
|
|
61
50
|
}
|
|
@@ -110,13 +99,17 @@ function resolveApplyLineResetsSlot(internals: ViewportTuiInternals): ApplyLineR
|
|
|
110
99
|
}
|
|
111
100
|
|
|
112
101
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
102
|
+
* Treats the hidden viewport prefix as immutable scrollback while frame size is
|
|
103
|
+
* stable. The next frame keeps the terminal's actual hidden text, so visible
|
|
104
|
+
* changes can use pi-tui's differential path instead of clearing and replaying
|
|
105
|
+
* the screen. Structural, resize and image changes retain the native path.
|
|
117
106
|
*/
|
|
118
107
|
export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
|
|
119
108
|
try {
|
|
109
|
+
// Native fullscreen has an application-owned fixed viewport. Its inherited
|
|
110
|
+
// applyLineResets method is not part of that renderer's diff path, so patching
|
|
111
|
+
// it would report a false-positive attachment on the shared TUI base class.
|
|
112
|
+
if (tui.mode === "fullscreen") return { active: false, detach() {} };
|
|
120
113
|
const internals = tui as unknown as ViewportTuiInternals;
|
|
121
114
|
const slot = resolveApplyLineResetsSlot(internals);
|
|
122
115
|
if (!slot) return { active: false, detach() {} };
|
|
@@ -137,6 +130,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
|
|
|
137
130
|
&& Number.isFinite(viewportTop)
|
|
138
131
|
&& viewportTop > 0
|
|
139
132
|
&& this.previousHeight === this.terminal?.rows
|
|
133
|
+
&& this.previousWidth === this.terminal?.columns
|
|
140
134
|
) {
|
|
141
135
|
const hiddenEnd = Math.min(previousLines.length, Math.trunc(viewportTop));
|
|
142
136
|
const hiddenHasKittyImage = previousLines.slice(0, hiddenEnd).some(isKittyImageLine)
|
|
@@ -144,10 +138,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
|
|
|
144
138
|
if (!hiddenHasKittyImage) {
|
|
145
139
|
for (let index = 0; index < hiddenEnd; index += 1) {
|
|
146
140
|
const previous = previousLines[index];
|
|
147
|
-
|
|
148
|
-
if (previous !== undefined && next !== undefined && differsOnlyByTrailingDuration(previous, next)) {
|
|
149
|
-
nextLines[index] = previous;
|
|
150
|
-
}
|
|
141
|
+
if (previous !== undefined) nextLines[index] = previous;
|
|
151
142
|
}
|
|
152
143
|
}
|
|
153
144
|
}
|