@aragon-agent/cli 0.6.34 → 0.6.36
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/CHANGELOG.md +21 -0
- package/README.md +173 -7
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +67 -3
- package/dist/agent/controller.js +106 -5
- package/dist/agent/headless.js +31 -0
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +135 -4
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/input/keymap.js +18 -4
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +414 -54
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +17 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +22 -2
- package/dist/ui/ScrollbackShell.d.ts +22 -7
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +18 -0
- package/dist/ui/StatusBar.js +11 -3
- package/dist/ui/TeamPanel.d.ts +35 -41
- package/dist/ui/TeamPanel.js +53 -163
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +23 -3
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +23 -3
- package/dist/ui/layout/header-hints.js +73 -7
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +32 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +24 -2
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/LiveTail.d.ts +2 -2
- package/dist/ui/scrollback/LiveTail.js +7 -2
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/tail-window.d.ts +12 -0
- package/dist/ui/scrollback/tail-window.js +17 -2
- package/dist/ui/scrollback/use-scrollback-view.js +7 -0
- package/dist/ui/team-status.d.ts +166 -0
- package/dist/ui/team-status.js +252 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +10 -2
- package/package.json +2 -2
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt manager's state machine and view models (quick-prompt-library §4.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`. Every sentence the
|
|
5
|
+
* user reads comes from `prompts/copy.ts`; nothing in this file may hold a literal
|
|
6
|
+
* non-ASCII byte.
|
|
7
|
+
*
|
|
8
|
+
* WHY A PURE REDUCER AND NOT `useState` INSIDE THE COMPONENT: the page stack has two
|
|
9
|
+
* transitions that are easy to get wrong and impossible to see on screen - "the
|
|
10
|
+
* second Esc discards, the first does not" and "the intent is consumed exactly
|
|
11
|
+
* once" - and both are assertions, not screenshots. Everything below can be driven
|
|
12
|
+
* from a test with no Ink mounted.
|
|
13
|
+
*/
|
|
14
|
+
import { PROMPT_LIMITS } from '../../prompts/limits.js';
|
|
15
|
+
import { parsePromptTags, validatePromptDraft } from '../../prompts/store.js';
|
|
16
|
+
// The list's read-only preview is the ONE place `wrapToRows` belongs: it is a prose
|
|
17
|
+
// wrapper, so it would destroy the caret-to-cell mapping in an editable field
|
|
18
|
+
// (D-11), and there is no caret here.
|
|
19
|
+
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
20
|
+
/** The six rows `Tab` cycles on the editor page, in visual order. */
|
|
21
|
+
export const PROMPT_EDITOR_FIELDS = [
|
|
22
|
+
'title', 'content', 'description', 'tags', 'save', 'cancel',
|
|
23
|
+
];
|
|
24
|
+
/** A new, empty draft. `id: null` is what makes `Save` a create rather than an update. */
|
|
25
|
+
export const EMPTY_PROMPT_DRAFT = {
|
|
26
|
+
id: null,
|
|
27
|
+
title: '',
|
|
28
|
+
content: '',
|
|
29
|
+
description: '',
|
|
30
|
+
tags: '',
|
|
31
|
+
favorite: false,
|
|
32
|
+
};
|
|
33
|
+
export const INITIAL_PROMPT_LIBRARY_STATE = {
|
|
34
|
+
page: 'list',
|
|
35
|
+
index: 0,
|
|
36
|
+
focusId: null,
|
|
37
|
+
draft: EMPTY_PROMPT_DRAFT,
|
|
38
|
+
baseline: null,
|
|
39
|
+
escArmed: false,
|
|
40
|
+
pendingDeleteId: null,
|
|
41
|
+
error: null,
|
|
42
|
+
errorField: null,
|
|
43
|
+
consumedIntentNonce: 0,
|
|
44
|
+
};
|
|
45
|
+
/** The editable projection of a stored entry. */
|
|
46
|
+
export function draftFromEntry(entry) {
|
|
47
|
+
return {
|
|
48
|
+
id: entry.id,
|
|
49
|
+
title: entry.title,
|
|
50
|
+
content: entry.content,
|
|
51
|
+
description: entry.description ?? '',
|
|
52
|
+
tags: (entry.tags ?? []).join(' '),
|
|
53
|
+
favorite: entry.favorite === true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** A create draft seeded from a command (`/prompts add title :: body`). */
|
|
57
|
+
export function seededDraft(seed) {
|
|
58
|
+
return {
|
|
59
|
+
...EMPTY_PROMPT_DRAFT,
|
|
60
|
+
title: seed?.title ?? '',
|
|
61
|
+
content: seed?.content ?? '',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Structural equality is enough: every field is a string or a boolean. */
|
|
65
|
+
export function draftIsDirty(draft, baseline) {
|
|
66
|
+
if (baseline === null) {
|
|
67
|
+
return draft.title.length > 0 || draft.content.length > 0
|
|
68
|
+
|| draft.description.length > 0 || draft.tags.length > 0;
|
|
69
|
+
}
|
|
70
|
+
return draft.title !== baseline.title || draft.content !== baseline.content
|
|
71
|
+
|| draft.description !== baseline.description || draft.tags !== baseline.tags
|
|
72
|
+
|| draft.favorite !== baseline.favorite;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate a draft and turn it into what the store accepts (D-12).
|
|
76
|
+
*
|
|
77
|
+
* THE VALIDATOR IS `store.ts`'S, not a second copy here: the sentence the editor
|
|
78
|
+
* shows and the rule the write applies have to be the same rule, or a user can be
|
|
79
|
+
* told a title is fine by the form and refused by the store.
|
|
80
|
+
*/
|
|
81
|
+
export function finishPromptEditor(draft) {
|
|
82
|
+
return validatePromptDraft({
|
|
83
|
+
title: draft.title,
|
|
84
|
+
content: draft.content,
|
|
85
|
+
description: draft.description,
|
|
86
|
+
tags: parsePromptTags(draft.tags),
|
|
87
|
+
favorite: draft.favorite,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Index of the first focusable row, for a refusal that named a field. */
|
|
91
|
+
export function fieldIndex(field) {
|
|
92
|
+
if (field === null)
|
|
93
|
+
return 0;
|
|
94
|
+
const at = PROMPT_EDITOR_FIELDS.indexOf(field);
|
|
95
|
+
return at < 0 ? 0 : at;
|
|
96
|
+
}
|
|
97
|
+
function enterEditor(state, draft, baseline) {
|
|
98
|
+
return {
|
|
99
|
+
...state,
|
|
100
|
+
page: 'editor',
|
|
101
|
+
draft,
|
|
102
|
+
baseline,
|
|
103
|
+
escArmed: false,
|
|
104
|
+
error: null,
|
|
105
|
+
errorField: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The reducer.
|
|
110
|
+
*
|
|
111
|
+
* `init` is the ONLY action that can jump straight to the editor or the delete page:
|
|
112
|
+
* that is what makes `/prompts edit 2` land where it should (D-21). The intent's
|
|
113
|
+
* nonce is stored as `consumedIntentNonce` by the component before dispatching, so a
|
|
114
|
+
* replayed intent is a no-op at the component, not a second edit here.
|
|
115
|
+
*/
|
|
116
|
+
export function promptLibraryReducer(state, action, library) {
|
|
117
|
+
switch (action.type) {
|
|
118
|
+
case 'init': {
|
|
119
|
+
if (action.nonce <= state.consumedIntentNonce)
|
|
120
|
+
return state;
|
|
121
|
+
const consumed = { ...state, consumedIntentNonce: action.nonce };
|
|
122
|
+
const entry = action.entry;
|
|
123
|
+
if (entry.kind === 'create') {
|
|
124
|
+
return enterEditor(consumed, seededDraft(entry.seed), null);
|
|
125
|
+
}
|
|
126
|
+
if (entry.kind === 'edit') {
|
|
127
|
+
const found = action.library.prompts.find((prompt) => prompt.id === entry.id);
|
|
128
|
+
if (!found)
|
|
129
|
+
return { ...consumed, page: 'list', focusId: null };
|
|
130
|
+
const draft = draftFromEntry(found);
|
|
131
|
+
return enterEditor(consumed, draft, { ...draft });
|
|
132
|
+
}
|
|
133
|
+
if (entry.kind === 'delete') {
|
|
134
|
+
const found = action.library.prompts.find((prompt) => prompt.id === entry.id);
|
|
135
|
+
if (!found)
|
|
136
|
+
return { ...consumed, page: 'list', focusId: null };
|
|
137
|
+
return { ...consumed, page: 'delete', pendingDeleteId: entry.id };
|
|
138
|
+
}
|
|
139
|
+
return { ...consumed, page: 'list', focusId: entry.focusId ?? null, index: 0 };
|
|
140
|
+
}
|
|
141
|
+
case 'list':
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
page: 'list',
|
|
145
|
+
focusId: action.focusId ?? state.focusId,
|
|
146
|
+
escArmed: false,
|
|
147
|
+
pendingDeleteId: null,
|
|
148
|
+
error: null,
|
|
149
|
+
errorField: null,
|
|
150
|
+
};
|
|
151
|
+
case 'focus':
|
|
152
|
+
return { ...state, index: Math.max(0, Math.floor(action.index)) };
|
|
153
|
+
case 'create':
|
|
154
|
+
return enterEditor(state, seededDraft(action.seed), null);
|
|
155
|
+
case 'edit': {
|
|
156
|
+
const found = action.library.prompts.find((prompt) => prompt.id === action.id);
|
|
157
|
+
if (!found)
|
|
158
|
+
return { ...state, page: 'list', error: 'not_found' };
|
|
159
|
+
const draft = draftFromEntry(found);
|
|
160
|
+
return enterEditor(state, draft, { ...draft });
|
|
161
|
+
}
|
|
162
|
+
case 'delete':
|
|
163
|
+
return { ...state, page: 'delete', pendingDeleteId: action.id };
|
|
164
|
+
case 'draft':
|
|
165
|
+
return { ...state, draft: { ...state.draft, ...action.patch }, escArmed: false };
|
|
166
|
+
case 'commit':
|
|
167
|
+
return {
|
|
168
|
+
...state,
|
|
169
|
+
page: 'list',
|
|
170
|
+
focusId: action.id,
|
|
171
|
+
baseline: null,
|
|
172
|
+
escArmed: false,
|
|
173
|
+
error: null,
|
|
174
|
+
errorField: null,
|
|
175
|
+
draft: EMPTY_PROMPT_DRAFT,
|
|
176
|
+
};
|
|
177
|
+
case 'failed':
|
|
178
|
+
return { ...state, error: action.code, errorField: action.field ?? null };
|
|
179
|
+
case 'esc': {
|
|
180
|
+
if (state.page !== 'editor')
|
|
181
|
+
return { ...state, page: 'list', pendingDeleteId: null };
|
|
182
|
+
// A CLEAN EDITOR CLOSES ON THE FIRST Esc. A dirty one asks once, and the answer
|
|
183
|
+
// has to be a second Esc rather than a modal: losing a body typed into a
|
|
184
|
+
// multi-line field because one key was pressed is the failure this guards.
|
|
185
|
+
if (!draftIsDirty(state.draft, state.baseline)) {
|
|
186
|
+
return { ...state, page: 'list', baseline: null, draft: EMPTY_PROMPT_DRAFT };
|
|
187
|
+
}
|
|
188
|
+
if (state.escArmed) {
|
|
189
|
+
return {
|
|
190
|
+
...state, page: 'list', baseline: null, escArmed: false, draft: EMPTY_PROMPT_DRAFT,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { ...state, escArmed: true };
|
|
194
|
+
}
|
|
195
|
+
case 'cancelled':
|
|
196
|
+
return { ...state, page: 'list', baseline: null, escArmed: false, draft: EMPTY_PROMPT_DRAFT };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** `1.2k` for anything a fixed-width column cannot show in full. */
|
|
200
|
+
export function formatCount(count) {
|
|
201
|
+
if (count < 1000)
|
|
202
|
+
return String(count);
|
|
203
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
204
|
+
}
|
|
205
|
+
/** `3d ago` / `2h ago` / `just now`. Deliberately coarse: it is a hint, not a clock. */
|
|
206
|
+
export function formatAgo(timestamp, now) {
|
|
207
|
+
if (timestamp === undefined || timestamp <= 0)
|
|
208
|
+
return 'never';
|
|
209
|
+
const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
|
|
210
|
+
if (seconds < 60)
|
|
211
|
+
return 'just now';
|
|
212
|
+
const minutes = Math.floor(seconds / 60);
|
|
213
|
+
if (minutes < 60)
|
|
214
|
+
return `${minutes}m ago`;
|
|
215
|
+
const hours = Math.floor(minutes / 60);
|
|
216
|
+
if (hours < 24)
|
|
217
|
+
return `${hours}h ago`;
|
|
218
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The list, one row per entry, in the library's own order.
|
|
222
|
+
*
|
|
223
|
+
* DUPLICATE TITLES GET AN ID PREFIX (I-2), exactly as `kit-picker-state.ts` does for
|
|
224
|
+
* duplicate kit names: two rows reading the same words make `Enter` a coin flip.
|
|
225
|
+
*/
|
|
226
|
+
export function libraryRows(library) {
|
|
227
|
+
const seen = new Map();
|
|
228
|
+
for (const prompt of library.prompts) {
|
|
229
|
+
seen.set(prompt.title, (seen.get(prompt.title) ?? 0) + 1);
|
|
230
|
+
}
|
|
231
|
+
return library.prompts.map((prompt) => {
|
|
232
|
+
const duplicate = (seen.get(prompt.title) ?? 0) > 1;
|
|
233
|
+
const identity = duplicate ? `#${prompt.id.slice(0, 8)} ` : '';
|
|
234
|
+
const marks = [];
|
|
235
|
+
if (prompt.favorite === true)
|
|
236
|
+
marks.push('[fav]');
|
|
237
|
+
const tags = prompt.tags ?? [];
|
|
238
|
+
for (const tag of tags.slice(0, 2))
|
|
239
|
+
marks.push(`#${tag}`);
|
|
240
|
+
if (tags.length > 2)
|
|
241
|
+
marks.push(`+#${tags.length - 2}`);
|
|
242
|
+
marks.push(`${formatCount([...prompt.content].length)}c`);
|
|
243
|
+
if (prompt.useCount > 0)
|
|
244
|
+
marks.push(`${formatCount(prompt.useCount)}x`);
|
|
245
|
+
return {
|
|
246
|
+
id: prompt.id,
|
|
247
|
+
label: `${identity}${prompt.title} ${marks.join(' ')}`,
|
|
248
|
+
markers: {
|
|
249
|
+
favorite: prompt.favorite === true,
|
|
250
|
+
hasTags: tags.length > 0,
|
|
251
|
+
uses: prompt.useCount,
|
|
252
|
+
},
|
|
253
|
+
chars: [...prompt.content].length,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The detail block under the list: at most `detailMaxLines` rows, fixed shape.
|
|
259
|
+
*
|
|
260
|
+
* FIXED, because `OverlayFrame`'s window is shared with the list (D-16): a body that
|
|
261
|
+
* participated in row height would squeeze six prompts into two visible rows. The
|
|
262
|
+
* full text has exactly one home, and it is the editor.
|
|
263
|
+
*/
|
|
264
|
+
export function libraryDetail(row, library, cols, now = Date.now()) {
|
|
265
|
+
if (!row)
|
|
266
|
+
return [];
|
|
267
|
+
const prompt = library.prompts.find((entry) => entry.id === row.id);
|
|
268
|
+
if (!prompt)
|
|
269
|
+
return [];
|
|
270
|
+
const tags = (prompt.tags ?? []).map((tag) => `#${tag}`).join(' ');
|
|
271
|
+
const facts = [
|
|
272
|
+
`${formatCount(row.chars)} chars`,
|
|
273
|
+
`${prompt.useCount} uses`,
|
|
274
|
+
formatAgo(prompt.lastUsedAt, now),
|
|
275
|
+
prompt.favorite === true ? '[fav]' : '',
|
|
276
|
+
].filter(Boolean).join(' ');
|
|
277
|
+
const body = prompt.content.split('\n').slice(0, PROMPT_LIMITS.detailPreviewLines).join('\n');
|
|
278
|
+
const preview = wrapToRows(body, Math.max(8, cols - 8))
|
|
279
|
+
.slice(0, PROMPT_LIMITS.detailPreviewLines);
|
|
280
|
+
return [prompt.title, ...(tags.length > 0 ? [tags] : []), facts, ...preview]
|
|
281
|
+
.slice(0, PROMPT_LIMITS.detailMaxLines);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The list page's footer.
|
|
285
|
+
*
|
|
286
|
+
* THE LAST CLAUSE IS ALWAYS THE WAY OUT (§9.5): a narrow terminal clips from the
|
|
287
|
+
* left, so the reader always keeps `Esc close`. The reorder keys are named here
|
|
288
|
+
* rather than in the row above it because they are the one affordance a user cannot
|
|
289
|
+
* discover by pressing a key they already know.
|
|
290
|
+
*/
|
|
291
|
+
export function libraryHint(state, count) {
|
|
292
|
+
if (state.page === 'delete')
|
|
293
|
+
return 'Enter delete | Esc cancel';
|
|
294
|
+
if (state.page === 'editor')
|
|
295
|
+
return editorHint(state);
|
|
296
|
+
if (count === 0)
|
|
297
|
+
return 'a create your first prompt | Esc close';
|
|
298
|
+
return 'Enter edit | a new | d delete | f favorite | ^up/^dn reorder | Esc close';
|
|
299
|
+
}
|
|
300
|
+
function editorHint(state) {
|
|
301
|
+
if (state.escArmed)
|
|
302
|
+
return 'Esc again discards the draft | Tab next field';
|
|
303
|
+
return 'Tab next field | Ctrl+S save | Esc close';
|
|
304
|
+
}
|
|
305
|
+
/** The delete page's four lines: what it is, how big, and what it costs. */
|
|
306
|
+
export function deleteDetail(library, id) {
|
|
307
|
+
const prompt = library.prompts.find((entry) => entry.id === id);
|
|
308
|
+
if (!prompt)
|
|
309
|
+
return [];
|
|
310
|
+
return [
|
|
311
|
+
prompt.title,
|
|
312
|
+
`${[...prompt.content].length} chars, ${prompt.content.split('\n').length} lines`,
|
|
313
|
+
'This cannot be undone.',
|
|
314
|
+
'Enter or y deletes it; Esc or n keeps it.',
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The draft as the store accepts it.
|
|
319
|
+
*
|
|
320
|
+
* THE TAGS FIELD IS PARSED HERE, ONCE, and nowhere else: the editor edits a raw
|
|
321
|
+
* string, the store validates a `string[]`, and a second parser between them is how
|
|
322
|
+
* `a,b` and `a b` come to mean different things depending on which surface saved it.
|
|
323
|
+
*/
|
|
324
|
+
export function draftToInput(draft) {
|
|
325
|
+
const input = {
|
|
326
|
+
title: draft.title,
|
|
327
|
+
content: draft.content,
|
|
328
|
+
tags: parsePromptTags(draft.tags),
|
|
329
|
+
favorite: draft.favorite,
|
|
330
|
+
};
|
|
331
|
+
const description = draft.description.trim();
|
|
332
|
+
if (description.length > 0)
|
|
333
|
+
input.description = description;
|
|
334
|
+
return input;
|
|
335
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt picker's view model (quick-prompt-library §4.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE ROWS ARE COMPUTED OUTSIDE THE COMPONENT for the reason `kit-picker-state.ts`
|
|
7
|
+
* records one surface over: the same library is rendered by two overlays and by
|
|
8
|
+
* `/prompts list`, and three independent row builders is how "the manager says four
|
|
9
|
+
* entries and the picker says five" becomes possible without a single failing test.
|
|
10
|
+
*/
|
|
11
|
+
import type { PromptLibrary, PromptSortView } from '../../prompts/types.js';
|
|
12
|
+
export interface PromptPickerRow {
|
|
13
|
+
id: string;
|
|
14
|
+
/** 1-based position in the CURRENT view, i.e. the number `1`-`9` addresses. */
|
|
15
|
+
index: number;
|
|
16
|
+
title: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
favorite: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface PromptPickerInput {
|
|
21
|
+
library: PromptLibrary;
|
|
22
|
+
query: string;
|
|
23
|
+
view: PromptSortView;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Filter the library and number the result.
|
|
27
|
+
*
|
|
28
|
+
* THE VIEW IS APPLIED FIRST, THEN THE FILTER: searching inside `recent` must return
|
|
29
|
+
* the recent order, not the manual one. Reversing the two would make `Ctrl+S` appear
|
|
30
|
+
* to do nothing whenever a query is active - which is exactly when the user is
|
|
31
|
+
* looking at the footer to see whether it worked.
|
|
32
|
+
*/
|
|
33
|
+
export declare function pickerRows(input: PromptPickerInput): PromptPickerRow[];
|
|
34
|
+
/**
|
|
35
|
+
* The block under the list: at most four rows, and never the whole body.
|
|
36
|
+
*
|
|
37
|
+
* The picker exists to CHOOSE. The full text belongs to the editor, and printing it
|
|
38
|
+
* here would push the chosen row off the frame that has to show it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pickerDetail(row: PromptPickerRow | undefined, library: PromptLibrary, now?: number): string[];
|
|
41
|
+
/**
|
|
42
|
+
* The empty-library guidance (AC-23).
|
|
43
|
+
*
|
|
44
|
+
* IT NAMES TWO WAYS OUT, because the picker is reachable from two directions: the
|
|
45
|
+
* user who typed `/prompts` on an empty library can press `a` here, and the user who
|
|
46
|
+
* does not want to type can press `Ctrl+P` later. Naming only one of them leaves the
|
|
47
|
+
* other audience pressing a key that does nothing.
|
|
48
|
+
*/
|
|
49
|
+
export declare const NO_PROMPTS_HINT = "No prompts yet. Press Ctrl+P to create one, or a to add one here.";
|
|
50
|
+
/**
|
|
51
|
+
* The footer, ordered by WHICH PATH SURVIVES ON THE WORST TERMINAL (§9.11).
|
|
52
|
+
*
|
|
53
|
+
* `Enter fill` comes before `^Enter fill & send` because a terminal without keyboard
|
|
54
|
+
* enhancement delivers `Ctrl+Enter` as a plain `Enter`: the reader who works left to
|
|
55
|
+
* right must meet the path that always works first. `tab manage` sits after the two
|
|
56
|
+
* fill actions for the same reason - it is navigation, not the point of the screen.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pickerHint(input: {
|
|
59
|
+
count: number;
|
|
60
|
+
query: string;
|
|
61
|
+
view: PromptSortView;
|
|
62
|
+
}): string;
|
|
63
|
+
/**
|
|
64
|
+
* The `1`-`9` shortcut, or `null`.
|
|
65
|
+
*
|
|
66
|
+
* A DIGIT IS ONLY A SHORTCUT WHILE THE QUERY IS EMPTY. Otherwise `3` is the third
|
|
67
|
+
* character of what the user is searching for, and stealing it would make numeric
|
|
68
|
+
* queries impossible to type - so the guard is the emptiness of the query, not the
|
|
69
|
+
* shape of the key.
|
|
70
|
+
*/
|
|
71
|
+
export declare function pickerDigitTarget(rows: readonly PromptPickerRow[], input: string): string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt picker's view model (quick-prompt-library §4.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE ROWS ARE COMPUTED OUTSIDE THE COMPONENT for the reason `kit-picker-state.ts`
|
|
7
|
+
* records one surface over: the same library is rendered by two overlays and by
|
|
8
|
+
* `/prompts list`, and three independent row builders is how "the manager says four
|
|
9
|
+
* entries and the picker says five" becomes possible without a single failing test.
|
|
10
|
+
*/
|
|
11
|
+
import { PROMPT_PICKER_LIMITS } from '../../prompts/limits.js';
|
|
12
|
+
import { searchPrompts, viewOrder } from '../../prompts/search.js';
|
|
13
|
+
import { formatAgo, formatCount } from './prompt-library-state.js';
|
|
14
|
+
/**
|
|
15
|
+
* Filter the library and number the result.
|
|
16
|
+
*
|
|
17
|
+
* THE VIEW IS APPLIED FIRST, THEN THE FILTER: searching inside `recent` must return
|
|
18
|
+
* the recent order, not the manual one. Reversing the two would make `Ctrl+S` appear
|
|
19
|
+
* to do nothing whenever a query is active - which is exactly when the user is
|
|
20
|
+
* looking at the footer to see whether it worked.
|
|
21
|
+
*/
|
|
22
|
+
export function pickerRows(input) {
|
|
23
|
+
const ordered = viewOrder(input.library.prompts, input.view);
|
|
24
|
+
return searchPrompts(ordered, input.query).map(({ prompt }, at) => ({
|
|
25
|
+
id: prompt.id,
|
|
26
|
+
index: at + 1,
|
|
27
|
+
title: prompt.title,
|
|
28
|
+
tags: prompt.tags ?? [],
|
|
29
|
+
favorite: prompt.favorite === true,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The block under the list: at most four rows, and never the whole body.
|
|
34
|
+
*
|
|
35
|
+
* The picker exists to CHOOSE. The full text belongs to the editor, and printing it
|
|
36
|
+
* here would push the chosen row off the frame that has to show it.
|
|
37
|
+
*/
|
|
38
|
+
export function pickerDetail(row, library, now = Date.now()) {
|
|
39
|
+
if (!row)
|
|
40
|
+
return [];
|
|
41
|
+
const prompt = library.prompts.find((entry) => entry.id === row.id);
|
|
42
|
+
if (!prompt)
|
|
43
|
+
return [];
|
|
44
|
+
const lines = [
|
|
45
|
+
`${formatCount([...prompt.content].length)} chars, `
|
|
46
|
+
+ `${prompt.content.split('\n').length} lines, ${formatAgo(prompt.lastUsedAt, now)}`,
|
|
47
|
+
];
|
|
48
|
+
if ((prompt.tags ?? []).length > 0)
|
|
49
|
+
lines.push((prompt.tags ?? []).map((t) => `#${t}`).join(' '));
|
|
50
|
+
const firstLine = prompt.content.split('\n')[0] ?? '';
|
|
51
|
+
if (firstLine.length > 0)
|
|
52
|
+
lines.push(firstLine);
|
|
53
|
+
if (prompt.description !== undefined)
|
|
54
|
+
lines.push(prompt.description);
|
|
55
|
+
return lines.slice(0, PROMPT_PICKER_LIMITS.detailMaxLines);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The empty-library guidance (AC-23).
|
|
59
|
+
*
|
|
60
|
+
* IT NAMES TWO WAYS OUT, because the picker is reachable from two directions: the
|
|
61
|
+
* user who typed `/prompts` on an empty library can press `a` here, and the user who
|
|
62
|
+
* does not want to type can press `Ctrl+P` later. Naming only one of them leaves the
|
|
63
|
+
* other audience pressing a key that does nothing.
|
|
64
|
+
*/
|
|
65
|
+
export const NO_PROMPTS_HINT = 'No prompts yet. Press Ctrl+P to create one, or a to add one here.';
|
|
66
|
+
/**
|
|
67
|
+
* The footer, ordered by WHICH PATH SURVIVES ON THE WORST TERMINAL (§9.11).
|
|
68
|
+
*
|
|
69
|
+
* `Enter fill` comes before `^Enter fill & send` because a terminal without keyboard
|
|
70
|
+
* enhancement delivers `Ctrl+Enter` as a plain `Enter`: the reader who works left to
|
|
71
|
+
* right must meet the path that always works first. `tab manage` sits after the two
|
|
72
|
+
* fill actions for the same reason - it is navigation, not the point of the screen.
|
|
73
|
+
*/
|
|
74
|
+
export function pickerHint(input) {
|
|
75
|
+
if (input.count === 0) {
|
|
76
|
+
return input.query.length > 0
|
|
77
|
+
? 'No match | Backspace to widen | Esc close'
|
|
78
|
+
: `${NO_PROMPTS_HINT} Esc close`;
|
|
79
|
+
}
|
|
80
|
+
return 'Enter fill | ^Enter fill & send | 1-9 pick | '
|
|
81
|
+
+ `^S sort: ${input.view} | tab manage | Esc close`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The `1`-`9` shortcut, or `null`.
|
|
85
|
+
*
|
|
86
|
+
* A DIGIT IS ONLY A SHORTCUT WHILE THE QUERY IS EMPTY. Otherwise `3` is the third
|
|
87
|
+
* character of what the user is searching for, and stealing it would make numeric
|
|
88
|
+
* queries impossible to type - so the guard is the emptiness of the query, not the
|
|
89
|
+
* shape of the key.
|
|
90
|
+
*/
|
|
91
|
+
export function pickerDigitTarget(rows, input) {
|
|
92
|
+
if (input.length !== 1)
|
|
93
|
+
return null;
|
|
94
|
+
const digit = input.charCodeAt(0) - 48;
|
|
95
|
+
if (digit < 1 || digit > PROMPT_PICKER_LIMITS.quickDigits)
|
|
96
|
+
return null;
|
|
97
|
+
return rows[digit - 1]?.id ?? null;
|
|
98
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* WHY THIS FILE EXISTS. Native scroll mode cannot print an entry until it settles, so
|
|
6
6
|
* the live region used to show `buildLivePreview`'s plain-text projection - and for a
|
|
7
|
-
* running dispatch that projection
|
|
8
|
-
*
|
|
7
|
+
* running dispatch that projection was four grey rows saying `Team: 2/4 running`. It
|
|
8
|
+
* was the "simple look" the feature was opened for. A text-only region can never fix
|
|
9
9
|
* it, which is why the region's content type changed from `readonly string[]` to a
|
|
10
10
|
* fixed-height box that can hold components.
|
|
11
11
|
*
|
|
@@ -3,7 +3,7 @@ import { Box } from 'ink';
|
|
|
3
3
|
import { EntryView, nowSecFor } from '../Transcript.js';
|
|
4
4
|
import { estimateEntryRows } from '../layout/virtual-window.js';
|
|
5
5
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
6
|
-
import {
|
|
6
|
+
import { isSlotless } from './tail-window.js';
|
|
7
7
|
import { resolveTailAnchor } from './tail-anchor.js';
|
|
8
8
|
/**
|
|
9
9
|
* The last `maxEntries` printable entries, each with its predecessor.
|
|
@@ -13,7 +13,12 @@ import { resolveTailAnchor } from './tail-anchor.js';
|
|
|
13
13
|
* separation row the transcript shows above the same card.
|
|
14
14
|
*/
|
|
15
15
|
export function selectTailEntries(entries, maxEntries) {
|
|
16
|
-
|
|
16
|
+
// A LIVE DISPATCH CARD TAKES NO SLOT (tui-subagent-double-roster A-2): the roster
|
|
17
|
+
// draws it and `EntryView` returns null for it, so the window must not spend one of
|
|
18
|
+
// its slots on an entry that is not on screen. `isSlotless` is `tail-window.ts`'s
|
|
19
|
+
// predicate - the one `selectTailWindow` and the print pump apply - so the box and
|
|
20
|
+
// `planOverflowPrint` cannot drift into different windows.
|
|
21
|
+
const printable = entries.filter((entry) => !isSlotless(entry));
|
|
17
22
|
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
18
23
|
if (limit === 0)
|
|
19
24
|
return [];
|
|
@@ -109,7 +109,20 @@ export function buildLiveBand(input) {
|
|
|
109
109
|
// now gets the pool too. The branch stays as the named expression of the older policy.
|
|
110
110
|
: Math.max(0, remaining - teamRows - railReserved);
|
|
111
111
|
const bandRows = fill ? remaining : Math.max(railReserved, teamRows + previewRows);
|
|
112
|
-
|
|
112
|
+
// THE RAIL NO LONGER OWNS THE BAND'S FULL HEIGHT WHEN A ROSTER IS DRAWN
|
|
113
|
+
// (tui-subagent-panel §4.1.2 / D-3). The roster is now a FULL-WIDTH slot below
|
|
114
|
+
// the rail's row region, so the two no longer overlap: the rail gets exactly the
|
|
115
|
+
// rows the roster does not take. With no roster this is `bandRows`, i.e.
|
|
116
|
+
// byte-identical to the previous behaviour (I-6).
|
|
117
|
+
//
|
|
118
|
+
// I-6a, WHY THIS CANNOT STARVE THE RAIL: `teamRows <= teamCeiling =
|
|
119
|
+
// min(teamRowsNatural, remaining - panelMinRows - 1)` (step 2 above) and in
|
|
120
|
+
// `fill` mode `bandRows === remaining`, so `railRows >= panelMinRows + 1 = 4`.
|
|
121
|
+
// Four satisfies both `TODO_LIMITS.panelMinRows(3)` (the rail stays mounted) and
|
|
122
|
+
// `SCROLLBACK_LIMITS.railBandMinRows(4)` (the band still reports
|
|
123
|
+
// `reason === 'visible'`). The price of a screen-right clock is a SHORTER rail
|
|
124
|
+
// during a dispatch, never a missing one - see §4.1.4's worked example.
|
|
125
|
+
const railRows = railVisible ? Math.max(0, bandRows - teamRows) : 0;
|
|
113
126
|
const railColsOut = railVisible ? railCols : 0;
|
|
114
127
|
return {
|
|
115
128
|
reason,
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { clampLiveText } from '../live-clamp.js';
|
|
21
21
|
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
22
22
|
import { isExcluded } from './settled-boundary.js';
|
|
23
|
+
import { isLiveTeamCard, outcomePhrase, rosterCounts } from '../team-status.js';
|
|
23
24
|
import { clipLine } from './writer-shapes.js';
|
|
24
25
|
const MAX_TEAM_RUN_ROWS = 3;
|
|
25
26
|
const TOOL_LIVE_ROWS = 6;
|
|
@@ -56,8 +57,10 @@ function toolRows(entry, cols) {
|
|
|
56
57
|
return out;
|
|
57
58
|
}
|
|
58
59
|
function teamRows(entry, cols) {
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// ONLY A SETTLED CARD REACHES HERE (the live one projects nothing, below), so the
|
|
61
|
+
// outcome phrase - `team-status.ts`'s own sentence - replaces an always-zero fraction.
|
|
62
|
+
const counts = rosterCounts(entry.runs);
|
|
63
|
+
const out = [fit(`Team: ${outcomePhrase(counts)}`, cols)];
|
|
61
64
|
for (const run of entry.runs.slice(0, MAX_TEAM_RUN_ROWS)) {
|
|
62
65
|
out.push(fit(` ${run.label} ${run.phase}`, cols));
|
|
63
66
|
}
|
|
@@ -71,7 +74,11 @@ function entryRows(entry, input) {
|
|
|
71
74
|
case 'tool':
|
|
72
75
|
return toolRows(entry, cols);
|
|
73
76
|
case 'team':
|
|
74
|
-
|
|
77
|
+
// THE LIVE DISPATCH HAS ONE SURFACE (tui-subagent-double-roster A-2): this
|
|
78
|
+
// projection is the text fallback for the same slot `LiveTail` fills, so a
|
|
79
|
+
// `Team: n/m running` line here would be the second live block again, one level
|
|
80
|
+
// down. A settled card still projects normally.
|
|
81
|
+
return isLiveTeamCard(entry) ? [] : teamRows(entry, cols);
|
|
75
82
|
case 'todo':
|
|
76
83
|
return [fit(`Todo: ${entry.doneCount}/${entry.total}`, cols)];
|
|
77
84
|
case 'compaction':
|
|
@@ -27,6 +27,18 @@
|
|
|
27
27
|
* closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
|
|
28
28
|
*/
|
|
29
29
|
import type { Entry } from '../../agent/reducer.js';
|
|
30
|
+
/**
|
|
31
|
+
* Entries that take NO slot in the window: they neither print nor take a row.
|
|
32
|
+
*
|
|
33
|
+
* A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
|
|
34
|
+
* (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
|
|
35
|
+
* transcript draws nothing, so the window must not spend one of its slots on an
|
|
36
|
+
* entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
|
|
37
|
+
* so both window functions below skip it, and `LiveTail.selectTailEntries` - the
|
|
38
|
+
* window function the box renders - imports this one instead of spelling the
|
|
39
|
+
* composition out a second time.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isSlotless(entry: Entry): boolean;
|
|
30
42
|
export interface TailWindow {
|
|
31
43
|
/** First index of the window inside the full entry list. */
|
|
32
44
|
start: number;
|
|
@@ -27,6 +27,21 @@
|
|
|
27
27
|
* closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
|
|
28
28
|
*/
|
|
29
29
|
import { isExcluded, settledCount } from './settled-boundary.js';
|
|
30
|
+
import { isLiveTeamCard } from '../team-status.js';
|
|
31
|
+
/**
|
|
32
|
+
* Entries that take NO slot in the window: they neither print nor take a row.
|
|
33
|
+
*
|
|
34
|
+
* A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
|
|
35
|
+
* (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
|
|
36
|
+
* transcript draws nothing, so the window must not spend one of its slots on an
|
|
37
|
+
* entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
|
|
38
|
+
* so both window functions below skip it, and `LiveTail.selectTailEntries` - the
|
|
39
|
+
* window function the box renders - imports this one instead of spelling the
|
|
40
|
+
* composition out a second time.
|
|
41
|
+
*/
|
|
42
|
+
export function isSlotless(entry) {
|
|
43
|
+
return isExcluded(entry) || isLiveTeamCard(entry);
|
|
44
|
+
}
|
|
30
45
|
/**
|
|
31
46
|
* The index of the first entry that is still on screen: count backwards up to
|
|
32
47
|
* `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
|
|
@@ -42,7 +57,7 @@ export function tailWindowStart(entries, maxEntries) {
|
|
|
42
57
|
return entries.length;
|
|
43
58
|
let seen = 0;
|
|
44
59
|
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
45
|
-
if (
|
|
60
|
+
if (isSlotless(entries[i]))
|
|
46
61
|
continue;
|
|
47
62
|
seen += 1;
|
|
48
63
|
if (seen === limit)
|
|
@@ -54,7 +69,7 @@ export function tailWindowStart(entries, maxEntries) {
|
|
|
54
69
|
/** `entries.slice(start)` with the entries that never print dropped. */
|
|
55
70
|
export function selectTailWindow(entries, maxEntries) {
|
|
56
71
|
const start = tailWindowStart(entries, maxEntries);
|
|
57
|
-
return { start, entries: entries.slice(start).filter((entry) => !
|
|
72
|
+
return { start, entries: entries.slice(start).filter((entry) => !isSlotless(entry)) };
|
|
58
73
|
}
|
|
59
74
|
/**
|
|
60
75
|
* The print boundary: never print what the region is still showing.
|