praana 0.5.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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/praana.js +17 -0
- package/bin/pran.js +17 -0
- package/dist/app-banner.d.ts +11 -0
- package/dist/app-banner.js +161 -0
- package/dist/app-controller.d.ts +44 -0
- package/dist/app-controller.js +143 -0
- package/dist/app-identity.d.ts +18 -0
- package/dist/app-identity.js +52 -0
- package/dist/auto-compact.d.ts +16 -0
- package/dist/auto-compact.js +101 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.js +69 -0
- package/dist/compile-classic.d.ts +21 -0
- package/dist/compile-classic.js +106 -0
- package/dist/compiler.d.ts +75 -0
- package/dist/compiler.js +406 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +433 -0
- package/dist/context-engine/activity-log.d.ts +9 -0
- package/dist/context-engine/activity-log.js +109 -0
- package/dist/context-engine/artifact-store.d.ts +32 -0
- package/dist/context-engine/artifact-store.js +272 -0
- package/dist/context-engine/bm25.d.ts +3 -0
- package/dist/context-engine/bm25.js +32 -0
- package/dist/context-engine/checkpoint.d.ts +34 -0
- package/dist/context-engine/checkpoint.js +430 -0
- package/dist/context-engine/classify.d.ts +3 -0
- package/dist/context-engine/classify.js +60 -0
- package/dist/context-engine/db.d.ts +73 -0
- package/dist/context-engine/db.js +505 -0
- package/dist/context-engine/distiller.d.ts +30 -0
- package/dist/context-engine/distiller.js +67 -0
- package/dist/context-engine/engine-compiler.d.ts +23 -0
- package/dist/context-engine/engine-compiler.js +297 -0
- package/dist/context-engine/error-tracker.d.ts +21 -0
- package/dist/context-engine/error-tracker.js +74 -0
- package/dist/context-engine/event-lineage.d.ts +26 -0
- package/dist/context-engine/event-lineage.js +120 -0
- package/dist/context-engine/extraction.d.ts +26 -0
- package/dist/context-engine/extraction.js +83 -0
- package/dist/context-engine/index.d.ts +82 -0
- package/dist/context-engine/index.js +238 -0
- package/dist/context-engine/scoring.d.ts +13 -0
- package/dist/context-engine/scoring.js +47 -0
- package/dist/context-engine/state-snapshot.d.ts +8 -0
- package/dist/context-engine/state-snapshot.js +50 -0
- package/dist/context-engine/summarize.d.ts +6 -0
- package/dist/context-engine/summarize.js +32 -0
- package/dist/context-engine/telemetry.d.ts +25 -0
- package/dist/context-engine/telemetry.js +64 -0
- package/dist/context-engine/turn-digest.d.ts +50 -0
- package/dist/context-engine/turn-digest.js +250 -0
- package/dist/context-engine/turn-ledger.d.ts +18 -0
- package/dist/context-engine/turn-ledger.js +184 -0
- package/dist/context-engine/turn-recorder.d.ts +24 -0
- package/dist/context-engine/turn-recorder.js +88 -0
- package/dist/context-engine/types.d.ts +201 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-pressure.d.ts +19 -0
- package/dist/context-pressure.js +36 -0
- package/dist/distillers/generic.d.ts +14 -0
- package/dist/distillers/generic.js +93 -0
- package/dist/distillers/git-diff.d.ts +8 -0
- package/dist/distillers/git-diff.js +119 -0
- package/dist/distillers/index.d.ts +2 -0
- package/dist/distillers/index.js +16 -0
- package/dist/distillers/npm-test.d.ts +8 -0
- package/dist/distillers/npm-test.js +50 -0
- package/dist/distillers/rg-results.d.ts +8 -0
- package/dist/distillers/rg-results.js +28 -0
- package/dist/distillers/tsc-errors.d.ts +8 -0
- package/dist/distillers/tsc-errors.js +52 -0
- package/dist/event-log.d.ts +56 -0
- package/dist/event-log.js +214 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +155 -0
- package/dist/logger.d.ts +94 -0
- package/dist/logger.js +287 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +54 -0
- package/dist/memory/confidence.d.ts +7 -0
- package/dist/memory/confidence.js +37 -0
- package/dist/memory/consolidation.d.ts +26 -0
- package/dist/memory/consolidation.js +166 -0
- package/dist/memory/db.d.ts +40 -0
- package/dist/memory/db.js +283 -0
- package/dist/memory/dedup.d.ts +6 -0
- package/dist/memory/dedup.js +50 -0
- package/dist/memory/embedder-factory.d.ts +3 -0
- package/dist/memory/embedder-factory.js +81 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.js +67 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/ollama-summarizer.d.ts +19 -0
- package/dist/memory/ollama-summarizer.js +72 -0
- package/dist/memory/openai-summarizer.d.ts +21 -0
- package/dist/memory/openai-summarizer.js +51 -0
- package/dist/memory/store.d.ts +61 -0
- package/dist/memory/store.js +502 -0
- package/dist/memory/summarizer-factory.d.ts +3 -0
- package/dist/memory/summarizer-factory.js +69 -0
- package/dist/memory/summarizer.d.ts +4 -0
- package/dist/memory/summarizer.js +112 -0
- package/dist/memory/types.d.ts +87 -0
- package/dist/memory/types.js +17 -0
- package/dist/model-context.d.ts +15 -0
- package/dist/model-context.js +212 -0
- package/dist/project-detector.d.ts +37 -0
- package/dist/project-detector.js +604 -0
- package/dist/render.d.ts +15 -0
- package/dist/render.js +46 -0
- package/dist/session.d.ts +118 -0
- package/dist/session.js +809 -0
- package/dist/skills/index.d.ts +69 -0
- package/dist/skills/index.js +885 -0
- package/dist/skills/types.d.ts +93 -0
- package/dist/skills/types.js +8 -0
- package/dist/slash-commands.d.ts +14 -0
- package/dist/slash-commands.js +301 -0
- package/dist/state-graph.d.ts +38 -0
- package/dist/state-graph.js +255 -0
- package/dist/status-bar.d.ts +54 -0
- package/dist/status-bar.js +184 -0
- package/dist/thinking-display.d.ts +21 -0
- package/dist/thinking-display.js +37 -0
- package/dist/tool-summary.d.ts +4 -0
- package/dist/tool-summary.js +67 -0
- package/dist/tools/index.d.ts +925 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/knowledge.d.ts +140 -0
- package/dist/tools/knowledge.js +260 -0
- package/dist/tools/memory.d.ts +39 -0
- package/dist/tools/memory.js +300 -0
- package/dist/tools/search-code.d.ts +134 -0
- package/dist/tools/search-code.js +390 -0
- package/dist/tools/system.d.ts +16 -0
- package/dist/tools/system.js +499 -0
- package/dist/tools/tool-def.d.ts +6 -0
- package/dist/tools/tool-def.js +3 -0
- package/dist/turn-control.d.ts +51 -0
- package/dist/turn-control.js +210 -0
- package/dist/turn.d.ts +20 -0
- package/dist/turn.js +624 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.js +4 -0
- package/dist/ui/readline-ui.d.ts +2 -0
- package/dist/ui/readline-ui.js +176 -0
- package/dist/ui/tui/app.d.ts +13 -0
- package/dist/ui/tui/app.js +270 -0
- package/dist/ui/tui/busy-indicator.d.ts +2 -0
- package/dist/ui/tui/busy-indicator.js +13 -0
- package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
- package/dist/ui/tui/components/gutter-rule.js +9 -0
- package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
- package/dist/ui/tui/components/inline-tool-row.js +8 -0
- package/dist/ui/tui/components/prompt-input.d.ts +20 -0
- package/dist/ui/tui/components/prompt-input.js +120 -0
- package/dist/ui/tui/components/system-line.d.ts +5 -0
- package/dist/ui/tui/components/system-line.js +6 -0
- package/dist/ui/tui/components/thinking-block.d.ts +11 -0
- package/dist/ui/tui/components/thinking-block.js +31 -0
- package/dist/ui/tui/components/toast-line.d.ts +4 -0
- package/dist/ui/tui/components/toast-line.js +8 -0
- package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
- package/dist/ui/tui/components/tool-result-line.js +6 -0
- package/dist/ui/tui/components/turn-footer.d.ts +5 -0
- package/dist/ui/tui/components/turn-footer.js +7 -0
- package/dist/ui/tui/components/user-block.d.ts +6 -0
- package/dist/ui/tui/components/user-block.js +6 -0
- package/dist/ui/tui/logo-banner.d.ts +5 -0
- package/dist/ui/tui/logo-banner.js +8 -0
- package/dist/ui/tui/markdown-render.d.ts +16 -0
- package/dist/ui/tui/markdown-render.js +218 -0
- package/dist/ui/tui/palette.d.ts +12 -0
- package/dist/ui/tui/palette.js +13 -0
- package/dist/ui/tui/reasoning-summary.d.ts +12 -0
- package/dist/ui/tui/reasoning-summary.js +27 -0
- package/dist/ui/tui/reducer.d.ts +92 -0
- package/dist/ui/tui/reducer.js +260 -0
- package/dist/ui/tui/run.d.ts +3 -0
- package/dist/ui/tui/run.js +40 -0
- package/dist/ui/tui/sink.d.ts +4 -0
- package/dist/ui/tui/sink.js +89 -0
- package/dist/ui/tui/status-bar-view.d.ts +5 -0
- package/dist/ui/tui/status-bar-view.js +44 -0
- package/dist/ui/tui/terminal-height.d.ts +12 -0
- package/dist/ui/tui/terminal-height.js +20 -0
- package/dist/ui/tui/terminal-width.d.ts +2 -0
- package/dist/ui/tui/terminal-width.js +5 -0
- package/dist/ui/tui/tool-display.d.ts +23 -0
- package/dist/ui/tui/tool-display.js +217 -0
- package/dist/ui/tui/transcript-line.d.ts +12 -0
- package/dist/ui/tui/transcript-line.js +43 -0
- package/dist/ui/tui/transcript-replay.d.ts +12 -0
- package/dist/ui/tui/transcript-replay.js +117 -0
- package/dist/ui-events.d.ts +39 -0
- package/dist/ui-events.js +33 -0
- package/dist/ui.d.ts +77 -0
- package/dist/ui.js +179 -0
- package/package.json +73 -0
- package/praana.config.example.toml +231 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
export class StateGraph {
|
|
3
|
+
objects = new Map();
|
|
4
|
+
touchedTurn = new Map(); // turn number when last touched
|
|
5
|
+
turnCount = 0;
|
|
6
|
+
clear() {
|
|
7
|
+
this.objects.clear();
|
|
8
|
+
this.touchedTurn.clear();
|
|
9
|
+
this.turnCount = 0;
|
|
10
|
+
}
|
|
11
|
+
create(kind, payload) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const obj = {
|
|
14
|
+
id: ulid(),
|
|
15
|
+
kind,
|
|
16
|
+
tier: "active",
|
|
17
|
+
payload,
|
|
18
|
+
created: now,
|
|
19
|
+
updated: now,
|
|
20
|
+
lastTouched: now,
|
|
21
|
+
};
|
|
22
|
+
this.objects.set(obj.id, obj);
|
|
23
|
+
this.touchedTurn.set(obj.id, this.turnCount);
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
update(id, patch) {
|
|
27
|
+
const obj = this.objects.get(id);
|
|
28
|
+
if (!obj)
|
|
29
|
+
return null;
|
|
30
|
+
obj.payload = { ...obj.payload, ...patch };
|
|
31
|
+
obj.updated = Date.now();
|
|
32
|
+
obj.lastTouched = Date.now();
|
|
33
|
+
this.touchedTurn.set(id, this.turnCount);
|
|
34
|
+
return obj;
|
|
35
|
+
}
|
|
36
|
+
setTier(id, tier) {
|
|
37
|
+
const obj = this.objects.get(id);
|
|
38
|
+
if (!obj)
|
|
39
|
+
return false;
|
|
40
|
+
obj.tier = tier;
|
|
41
|
+
obj.lastTouched = Date.now();
|
|
42
|
+
this.touchedTurn.set(id, this.turnCount);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
retractObject(id) {
|
|
46
|
+
const obj = this.objects.get(id);
|
|
47
|
+
if (!obj)
|
|
48
|
+
return false;
|
|
49
|
+
obj.retracted = true;
|
|
50
|
+
obj.updated = Date.now();
|
|
51
|
+
obj.lastTouched = Date.now();
|
|
52
|
+
this.touchedTurn.set(id, this.turnCount);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
get(id) {
|
|
56
|
+
return this.objects.get(id);
|
|
57
|
+
}
|
|
58
|
+
/** All active objects; focused object first, then by created asc, id asc. */
|
|
59
|
+
getActive() {
|
|
60
|
+
return [...this.objects.values()]
|
|
61
|
+
.filter((o) => o.tier === "active" && !o.retracted)
|
|
62
|
+
.sort((a, b) => {
|
|
63
|
+
const af = a.focused ? 1 : 0;
|
|
64
|
+
const bf = b.focused ? 1 : 0;
|
|
65
|
+
if (bf !== af)
|
|
66
|
+
return bf - af;
|
|
67
|
+
return a.created - b.created || (a.id < b.id ? -1 : 1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Pin one task/object as focused; clears focus on all others. */
|
|
71
|
+
setFocus(id) {
|
|
72
|
+
const target = this.objects.get(id);
|
|
73
|
+
if (!target)
|
|
74
|
+
return false;
|
|
75
|
+
for (const obj of this.objects.values()) {
|
|
76
|
+
obj.focused = obj.id === id;
|
|
77
|
+
}
|
|
78
|
+
target.lastTouched = Date.now();
|
|
79
|
+
this.touchedTurn.set(id, this.turnCount);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** All soft + hard objects, sorted by updated desc. */
|
|
83
|
+
getPeripheral() {
|
|
84
|
+
return [...this.objects.values()]
|
|
85
|
+
.filter((o) => (o.tier === "soft" || o.tier === "hard") && !o.retracted)
|
|
86
|
+
.sort((a, b) => b.updated - a.updated);
|
|
87
|
+
}
|
|
88
|
+
/** All objects with summary. */
|
|
89
|
+
list() {
|
|
90
|
+
return [...this.objects.values()]
|
|
91
|
+
.filter((o) => !o.retracted)
|
|
92
|
+
.sort((a, b) => a.created - b.created || (a.id < b.id ? -1 : 1))
|
|
93
|
+
.map((o) => ({
|
|
94
|
+
id: o.id,
|
|
95
|
+
kind: o.kind,
|
|
96
|
+
tier: o.tier,
|
|
97
|
+
summary: summarizePayload(o),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/** Full snapshot including retracted objects — for event logging and audit. */
|
|
101
|
+
snapshot() {
|
|
102
|
+
return [...this.objects.values()].sort((a, b) => a.created - b.created || (a.id < b.id ? -1 : 1));
|
|
103
|
+
}
|
|
104
|
+
getTouchedTurn(id) {
|
|
105
|
+
return this.touchedTurn.get(id) ?? 0;
|
|
106
|
+
}
|
|
107
|
+
incrementTurn() {
|
|
108
|
+
this.turnCount++;
|
|
109
|
+
}
|
|
110
|
+
getTurnCount() {
|
|
111
|
+
return this.turnCount;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Auto-promote peripheral objects whose payload matches keywords in the query.
|
|
115
|
+
* Returns IDs of objects that were hydrated.
|
|
116
|
+
*/
|
|
117
|
+
autoHydrate(query) {
|
|
118
|
+
const keywords = extractKeywords(query);
|
|
119
|
+
if (keywords.length === 0)
|
|
120
|
+
return [];
|
|
121
|
+
const hydrated = [];
|
|
122
|
+
for (const obj of this.getPeripheral()) {
|
|
123
|
+
const text = payloadToSearchableText(obj).toLowerCase();
|
|
124
|
+
if (keywords.some((kw) => text.includes(kw))) {
|
|
125
|
+
obj.tier = "active";
|
|
126
|
+
obj.lastTouched = Date.now();
|
|
127
|
+
this.touchedTurn.set(obj.id, this.turnCount);
|
|
128
|
+
hydrated.push(obj.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return hydrated;
|
|
132
|
+
}
|
|
133
|
+
/** Rebuild state from context_action events during resume. */
|
|
134
|
+
replayAction(payload) {
|
|
135
|
+
const action = payload.action;
|
|
136
|
+
const id = payload.id;
|
|
137
|
+
switch (action) {
|
|
138
|
+
case "create":
|
|
139
|
+
this.objects.set(id, {
|
|
140
|
+
id,
|
|
141
|
+
kind: payload.kind,
|
|
142
|
+
tier: payload.tier ?? "active",
|
|
143
|
+
payload: payload.statePayload,
|
|
144
|
+
created: payload.created,
|
|
145
|
+
updated: payload.updated,
|
|
146
|
+
lastTouched: payload.lastTouched,
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
case "update": {
|
|
150
|
+
const obj = this.objects.get(id);
|
|
151
|
+
if (obj && payload.statePayload) {
|
|
152
|
+
obj.payload = {
|
|
153
|
+
...obj.payload,
|
|
154
|
+
...payload.statePayload,
|
|
155
|
+
};
|
|
156
|
+
obj.updated = payload.updated;
|
|
157
|
+
obj.lastTouched = payload.lastTouched;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case "setTier": {
|
|
162
|
+
const obj2 = this.objects.get(id);
|
|
163
|
+
if (obj2) {
|
|
164
|
+
obj2.tier = payload.tier;
|
|
165
|
+
obj2.lastTouched = payload.lastTouched;
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "setFocus": {
|
|
170
|
+
const obj3 = this.objects.get(id);
|
|
171
|
+
if (obj3) {
|
|
172
|
+
for (const o of this.objects.values()) {
|
|
173
|
+
o.focused = o.id === id;
|
|
174
|
+
}
|
|
175
|
+
obj3.lastTouched = payload.lastTouched;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "retract": {
|
|
180
|
+
const obj4 = this.objects.get(id);
|
|
181
|
+
if (obj4) {
|
|
182
|
+
obj4.retracted = true;
|
|
183
|
+
obj4.updated = payload.updated;
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ---- Helpers ----
|
|
191
|
+
function summarizePayload(obj) {
|
|
192
|
+
switch (obj.kind) {
|
|
193
|
+
case "task": {
|
|
194
|
+
const p = obj.payload;
|
|
195
|
+
return `${p.status}: ${p.title}`;
|
|
196
|
+
}
|
|
197
|
+
case "decision": {
|
|
198
|
+
const p = obj.payload;
|
|
199
|
+
return p.summary;
|
|
200
|
+
}
|
|
201
|
+
case "constraint": {
|
|
202
|
+
const p = obj.payload;
|
|
203
|
+
return p.text.length > 80 ? p.text.slice(0, 80) + "..." : p.text;
|
|
204
|
+
}
|
|
205
|
+
case "note": {
|
|
206
|
+
const p = obj.payload;
|
|
207
|
+
return p.text.length > 80 ? p.text.slice(0, 80) + "..." : p.text;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export function summarizePayloadFn(obj) {
|
|
212
|
+
return summarizePayload(obj);
|
|
213
|
+
}
|
|
214
|
+
// ---- Auto-hydrate helpers ----
|
|
215
|
+
const STOP_WORDS = new Set([
|
|
216
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
217
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
218
|
+
"should", "may", "might", "must", "shall", "can", "need", "ought",
|
|
219
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
|
|
220
|
+
"into", "through", "during", "before", "after", "above", "below",
|
|
221
|
+
"between", "under", "again", "further", "then", "once", "here",
|
|
222
|
+
"there", "when", "where", "why", "how", "all", "each", "few", "more",
|
|
223
|
+
"most", "other", "some", "such", "no", "nor", "not", "only", "own",
|
|
224
|
+
"same", "so", "than", "too", "very", "just", "and", "but", "if", "or",
|
|
225
|
+
"because", "until", "while", "what", "which", "who", "whom", "this",
|
|
226
|
+
"that", "these", "those", "am", "it", "its", "they", "them", "their",
|
|
227
|
+
"i", "me", "my", "we", "our", "you", "your", "he", "him", "his", "she",
|
|
228
|
+
"her", "hers",
|
|
229
|
+
]);
|
|
230
|
+
function extractKeywords(query) {
|
|
231
|
+
return query
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.split(/[^a-z0-9]+/)
|
|
234
|
+
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
|
|
235
|
+
}
|
|
236
|
+
function payloadToSearchableText(obj) {
|
|
237
|
+
switch (obj.kind) {
|
|
238
|
+
case "task": {
|
|
239
|
+
const p = obj.payload;
|
|
240
|
+
return [p.title, p.description ?? ""].join(" ");
|
|
241
|
+
}
|
|
242
|
+
case "decision": {
|
|
243
|
+
const p = obj.payload;
|
|
244
|
+
return [p.summary, p.rationale].join(" ");
|
|
245
|
+
}
|
|
246
|
+
case "constraint": {
|
|
247
|
+
const p = obj.payload;
|
|
248
|
+
return p.text;
|
|
249
|
+
}
|
|
250
|
+
case "note": {
|
|
251
|
+
const p = obj.payload;
|
|
252
|
+
return p.text;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session status bar rendered above the readline prompt (stderr).
|
|
3
|
+
* Surfaces model, context usage, mode, repo, memory tiers, skills, and current task.
|
|
4
|
+
*/
|
|
5
|
+
import type { CompileMetrics } from "./compiler.js";
|
|
6
|
+
import type { Session } from "./session.js";
|
|
7
|
+
import type { StateGraph } from "./state-graph.js";
|
|
8
|
+
/** Default model context window when provider metadata is unavailable. */
|
|
9
|
+
export declare const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
10
|
+
export interface StatusBarInput {
|
|
11
|
+
model: string;
|
|
12
|
+
repoPath: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
debug: boolean;
|
|
15
|
+
thinking: boolean;
|
|
16
|
+
memoryEnabled: boolean;
|
|
17
|
+
incognito: boolean;
|
|
18
|
+
contextUsedTokens: number;
|
|
19
|
+
contextWindowTokens: number;
|
|
20
|
+
memoryStats: {
|
|
21
|
+
active: number;
|
|
22
|
+
soft: number;
|
|
23
|
+
hard: number;
|
|
24
|
+
};
|
|
25
|
+
skills: string[];
|
|
26
|
+
skillResidency: {
|
|
27
|
+
hot: number;
|
|
28
|
+
warm: number;
|
|
29
|
+
total: number;
|
|
30
|
+
} | null;
|
|
31
|
+
currentTask: string | null;
|
|
32
|
+
agentsContextLoaded: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Format token counts for compact display (e.g. 18400 → "18.4k"). */
|
|
35
|
+
export declare function formatTokenCount(tokens: number): string;
|
|
36
|
+
/** Short repo label: git root folder name, or cwd folder name. */
|
|
37
|
+
export declare function formatRepoLabel(repoPath: string, cwd: string): string;
|
|
38
|
+
/** Mode string for the status bar (debug + thinking visibility). */
|
|
39
|
+
export declare function formatMode(debug: boolean, thinking: boolean): string;
|
|
40
|
+
/** Pick the current task title: doing first, then oldest todo. */
|
|
41
|
+
export declare function getCurrentTaskTitle(stateGraph: StateGraph): string | null;
|
|
42
|
+
export declare function buildStatusBarInput(session: Session, opts: {
|
|
43
|
+
model: string;
|
|
44
|
+
debug: boolean;
|
|
45
|
+
thinking: boolean;
|
|
46
|
+
contextWindowTokens?: number;
|
|
47
|
+
compileMetrics?: CompileMetrics | null;
|
|
48
|
+
}): StatusBarInput;
|
|
49
|
+
/** Render status bar lines (no trailing newline on last line — caller adds newline). */
|
|
50
|
+
export declare function formatStatusBarLines(input: StatusBarInput): string[];
|
|
51
|
+
/** Write the status bar to stderr (no-op when not a TTY). */
|
|
52
|
+
export declare function renderStatusBar(input: StatusBarInput): void;
|
|
53
|
+
/** One-line emoji status bar — compact with pipe separators. */
|
|
54
|
+
export declare function formatEmojiStatusLine(input: StatusBarInput): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session status bar rendered above the readline prompt (stderr).
|
|
3
|
+
* Surfaces model, context usage, mode, repo, memory tiers, skills, and current task.
|
|
4
|
+
*/
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
/** Default model context window when provider metadata is unavailable. */
|
|
8
|
+
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
9
|
+
/** Format token counts for compact display (e.g. 18400 → "18.4k"). */
|
|
10
|
+
export function formatTokenCount(tokens) {
|
|
11
|
+
if (tokens < 1000)
|
|
12
|
+
return String(tokens);
|
|
13
|
+
if (tokens < 1_000_000) {
|
|
14
|
+
const k = tokens / 1000;
|
|
15
|
+
if (k >= 100 && Number.isInteger(k))
|
|
16
|
+
return `${k}k`;
|
|
17
|
+
if (k >= 100)
|
|
18
|
+
return `${Math.round(k)}k`;
|
|
19
|
+
const fixed = k.toFixed(1);
|
|
20
|
+
return `${fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed}k`;
|
|
21
|
+
}
|
|
22
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
23
|
+
}
|
|
24
|
+
/** Short repo label: git root folder name, or cwd folder name. */
|
|
25
|
+
export function formatRepoLabel(repoPath, cwd) {
|
|
26
|
+
const rootName = basename(repoPath);
|
|
27
|
+
const cwdName = basename(cwd);
|
|
28
|
+
if (repoPath === cwd || rootName === cwdName)
|
|
29
|
+
return rootName;
|
|
30
|
+
return `${rootName}/${cwdName}`;
|
|
31
|
+
}
|
|
32
|
+
/** Mode string for the status bar (debug + thinking visibility). */
|
|
33
|
+
export function formatMode(debug, thinking) {
|
|
34
|
+
if (debug && thinking)
|
|
35
|
+
return "debug+think";
|
|
36
|
+
if (debug)
|
|
37
|
+
return "debug";
|
|
38
|
+
if (!thinking)
|
|
39
|
+
return "normal·think-off";
|
|
40
|
+
return "normal";
|
|
41
|
+
}
|
|
42
|
+
/** Pick the current task title: doing first, then oldest todo. */
|
|
43
|
+
export function getCurrentTaskTitle(stateGraph) {
|
|
44
|
+
const tasks = stateGraph
|
|
45
|
+
.list()
|
|
46
|
+
.filter((o) => o.kind === "task")
|
|
47
|
+
.map((o) => {
|
|
48
|
+
const obj = stateGraph.get(o.id);
|
|
49
|
+
return obj ? obj.payload : null;
|
|
50
|
+
})
|
|
51
|
+
.filter((p) => p !== null);
|
|
52
|
+
const doing = tasks.find((t) => t.status === "doing");
|
|
53
|
+
if (doing)
|
|
54
|
+
return doing.title;
|
|
55
|
+
const todo = tasks.find((t) => t.status === "todo");
|
|
56
|
+
return todo?.title ?? null;
|
|
57
|
+
}
|
|
58
|
+
export function buildStatusBarInput(session, opts) {
|
|
59
|
+
const mem = session.getMemoryStats();
|
|
60
|
+
const metrics = opts.compileMetrics ?? session.getLastCompileMetrics();
|
|
61
|
+
const agentsContextTokens = session.agentsContext
|
|
62
|
+
? Math.ceil(session.agentsContext.length / 4)
|
|
63
|
+
: 0;
|
|
64
|
+
const skillResidency = session.skillRuntime?.getResidencyCounts();
|
|
65
|
+
return {
|
|
66
|
+
model: opts.model,
|
|
67
|
+
repoPath: session.getRepoRoot(),
|
|
68
|
+
cwd: session.cwd,
|
|
69
|
+
debug: opts.debug,
|
|
70
|
+
thinking: opts.thinking,
|
|
71
|
+
memoryEnabled: session.memoryEnabled,
|
|
72
|
+
incognito: session.isIncognito(),
|
|
73
|
+
contextUsedTokens: metrics?.totalTokens ?? agentsContextTokens,
|
|
74
|
+
contextWindowTokens: opts.contextWindowTokens ?? DEFAULT_CONTEXT_WINDOW,
|
|
75
|
+
memoryStats: { active: mem.active, soft: mem.soft, hard: mem.hard },
|
|
76
|
+
skills: (session.skills ?? []).map((s) => s.name),
|
|
77
|
+
skillResidency: skillResidency
|
|
78
|
+
? { hot: skillResidency.hot, warm: skillResidency.warm, total: skillResidency.hot + skillResidency.warm + skillResidency.cold }
|
|
79
|
+
: null,
|
|
80
|
+
currentTask: getCurrentTaskTitle(session.stateGraph),
|
|
81
|
+
agentsContextLoaded: !!session.agentsContext,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Render status bar lines (no trailing newline on last line — caller adds newline). */
|
|
85
|
+
export function formatStatusBarLines(input) {
|
|
86
|
+
const ctx = `${formatTokenCount(input.contextUsedTokens)} / ${formatTokenCount(input.contextWindowTokens)}`;
|
|
87
|
+
const repo = formatRepoLabel(input.repoPath, input.cwd);
|
|
88
|
+
const memFlag = input.incognito
|
|
89
|
+
? chalk.magenta("incognito")
|
|
90
|
+
: input.memoryEnabled
|
|
91
|
+
? chalk.green("on")
|
|
92
|
+
: chalk.dim("off");
|
|
93
|
+
const agents = input.agentsContextLoaded ? chalk.dim("· AGENTS.md") : "";
|
|
94
|
+
const line1 = [
|
|
95
|
+
chalk.cyan(input.model.split("/").pop() ?? input.model),
|
|
96
|
+
chalk.dim(ctx),
|
|
97
|
+
chalk.yellow(formatMode(input.debug, input.thinking)),
|
|
98
|
+
chalk.blue(repo),
|
|
99
|
+
`memory ${memFlag}${agents}`,
|
|
100
|
+
].join(chalk.dim(" · "));
|
|
101
|
+
const line2 = [
|
|
102
|
+
chalk.bold("Memory:"),
|
|
103
|
+
`${input.memoryStats.active} active`,
|
|
104
|
+
`${input.memoryStats.soft} soft`,
|
|
105
|
+
`${input.memoryStats.hard} hard`,
|
|
106
|
+
].join(chalk.dim(" · "));
|
|
107
|
+
const line3 = [chalk.bold("Context:"), ctx].join(" ");
|
|
108
|
+
const skillsLabel = input.skills.length > 0
|
|
109
|
+
? `${input.skills.length} skills (${input.skillResidency ? `${input.skillResidency.hot} HOT · ${input.skillResidency.warm} WARM · ${input.skillResidency.total - input.skillResidency.hot - input.skillResidency.warm} COLD` : "?/?/? loaded"})`
|
|
110
|
+
: chalk.dim("(none — add skills/*.md to project root)");
|
|
111
|
+
const line4 = [chalk.bold("Skills:"), skillsLabel].join(" ");
|
|
112
|
+
const taskLabel = input.currentTask
|
|
113
|
+
? chalk.white(input.currentTask)
|
|
114
|
+
: chalk.dim("(none — create_task)");
|
|
115
|
+
const line5 = [chalk.bold("Current task:"), taskLabel].join(" ");
|
|
116
|
+
return [line1, line2, line3, line4, line5];
|
|
117
|
+
}
|
|
118
|
+
/** Write the status bar to stderr (no-op when not a TTY). */
|
|
119
|
+
export function renderStatusBar(input) {
|
|
120
|
+
if (!process.stderr.isTTY)
|
|
121
|
+
return;
|
|
122
|
+
const width = process.stderr.columns ?? 80;
|
|
123
|
+
const lines = formatStatusBarLines(input).map((line) => truncateAnsiLine(line, width));
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
process.stderr.write(line + "\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** One-line emoji status bar — compact with pipe separators. */
|
|
129
|
+
export function formatEmojiStatusLine(input) {
|
|
130
|
+
const modelShort = input.model.split("/").pop() ?? input.model;
|
|
131
|
+
const pct = input.contextWindowTokens > 0
|
|
132
|
+
? Math.min(100, Math.round((input.contextUsedTokens / input.contextWindowTokens) * 100))
|
|
133
|
+
: 0;
|
|
134
|
+
const memStr = input.incognito ? "incognito" : input.memoryEnabled ? "on" : "off";
|
|
135
|
+
const skillsCount = input.skills.length;
|
|
136
|
+
let stateStr = "";
|
|
137
|
+
if (input.memoryStats && (input.memoryStats.active > 0 || input.memoryStats.soft > 0 || input.memoryStats.hard > 0)) {
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (input.memoryStats.active > 0)
|
|
140
|
+
parts.push(`${input.memoryStats.active}A`);
|
|
141
|
+
if (input.memoryStats.soft > 0)
|
|
142
|
+
parts.push(`${input.memoryStats.soft}S`);
|
|
143
|
+
if (input.memoryStats.hard > 0)
|
|
144
|
+
parts.push(`${input.memoryStats.hard}H`);
|
|
145
|
+
stateStr = parts.join("/");
|
|
146
|
+
}
|
|
147
|
+
const parts = [
|
|
148
|
+
chalk.cyan(`📦 model: ${input.model}`),
|
|
149
|
+
pct > 90 ? chalk.red(`🧠 ctx: ${pct}%`) : pct > 70 ? chalk.yellow(`🧠 ctx: ${pct}%`) : chalk.dim(`🧠 ctx: ${pct}%`),
|
|
150
|
+
memStr === "on" ? chalk.green(`💾 mem: ${memStr}`) : chalk.dim(`💾 mem: ${memStr}`),
|
|
151
|
+
];
|
|
152
|
+
if (skillsCount > 0) {
|
|
153
|
+
parts.push(chalk.magenta(`🛠️ ${skillsCount}sk${stateStr ? ` [${stateStr}]` : ""}`));
|
|
154
|
+
}
|
|
155
|
+
else if (stateStr) {
|
|
156
|
+
parts.push(chalk.magenta(`🛠️ [${stateStr}]`));
|
|
157
|
+
}
|
|
158
|
+
if (input.currentTask) {
|
|
159
|
+
parts.push(chalk.dim(`🎯 ${input.currentTask}`));
|
|
160
|
+
}
|
|
161
|
+
return parts.join(chalk.dim(" | "));
|
|
162
|
+
}
|
|
163
|
+
function truncateAnsiLine(line, maxWidth) {
|
|
164
|
+
const visible = stripAnsi(line);
|
|
165
|
+
if (visible.length <= maxWidth)
|
|
166
|
+
return line;
|
|
167
|
+
const suffix = "…";
|
|
168
|
+
const target = Math.max(10, maxWidth - suffix.length);
|
|
169
|
+
let cut = 0;
|
|
170
|
+
let vis = 0;
|
|
171
|
+
while (cut < line.length && vis < target) {
|
|
172
|
+
if (line[cut] === "\x1b") {
|
|
173
|
+
const end = line.indexOf("m", cut);
|
|
174
|
+
cut = end >= 0 ? end + 1 : cut + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
cut++;
|
|
178
|
+
vis++;
|
|
179
|
+
}
|
|
180
|
+
return line.slice(0, cut) + chalk.dim(suffix);
|
|
181
|
+
}
|
|
182
|
+
function stripAnsi(s) {
|
|
183
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
184
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the thinking block display.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from main.ts so the buffering / summary / toggle logic
|
|
5
|
+
* can be unit-tested without mocking process.stdout or stdin.
|
|
6
|
+
*/
|
|
7
|
+
export interface ThinkingState {
|
|
8
|
+
open: boolean;
|
|
9
|
+
buffer: string;
|
|
10
|
+
visible: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createThinkingState(initialVisible: boolean): ThinkingState;
|
|
13
|
+
/** Append a thinking delta to the buffer. Returns whether the header should be printed. */
|
|
14
|
+
export declare function onThinkingDelta(state: ThinkingState, delta: string): {
|
|
15
|
+
printHeader: boolean;
|
|
16
|
+
printDelta: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Close the thinking block. Returns the summary line (or null if empty). */
|
|
19
|
+
export declare function closeThinking(state: ThinkingState): string | null;
|
|
20
|
+
/** Toggle visibility. Returns the new state. */
|
|
21
|
+
export declare function toggleThinking(state: ThinkingState): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the thinking block display.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from main.ts so the buffering / summary / toggle logic
|
|
5
|
+
* can be unit-tested without mocking process.stdout or stdin.
|
|
6
|
+
*/
|
|
7
|
+
export function createThinkingState(initialVisible) {
|
|
8
|
+
return { open: false, buffer: "", visible: initialVisible };
|
|
9
|
+
}
|
|
10
|
+
/** Append a thinking delta to the buffer. Returns whether the header should be printed. */
|
|
11
|
+
export function onThinkingDelta(state, delta) {
|
|
12
|
+
if (!state.visible) {
|
|
13
|
+
return { printHeader: false, printDelta: false };
|
|
14
|
+
}
|
|
15
|
+
state.buffer += delta;
|
|
16
|
+
if (!state.open) {
|
|
17
|
+
state.open = true;
|
|
18
|
+
return { printHeader: true, printDelta: true };
|
|
19
|
+
}
|
|
20
|
+
return { printHeader: false, printDelta: true };
|
|
21
|
+
}
|
|
22
|
+
/** Close the thinking block. Returns the summary line (or null if empty). */
|
|
23
|
+
export function closeThinking(state) {
|
|
24
|
+
if (!state.open)
|
|
25
|
+
return null;
|
|
26
|
+
const summary = state.buffer.trim().length > 0
|
|
27
|
+
? ` [thinking: ${state.buffer.trim().length} chars]`
|
|
28
|
+
: null;
|
|
29
|
+
state.open = false;
|
|
30
|
+
state.buffer = "";
|
|
31
|
+
return summary;
|
|
32
|
+
}
|
|
33
|
+
/** Toggle visibility. Returns the new state. */
|
|
34
|
+
export function toggleThinking(state) {
|
|
35
|
+
state.visible = !state.visible;
|
|
36
|
+
return state.visible;
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Compact summaries for tool call / result display in terminal and TUI. */
|
|
2
|
+
export declare function formatToolResultRawText(result: unknown): string;
|
|
3
|
+
export declare function summarizeArgs(toolName: string, args: Record<string, unknown>): string;
|
|
4
|
+
export declare function summarizeResult(result: unknown): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Compact summaries for tool call / result display in terminal and TUI. */
|
|
2
|
+
export function formatToolResultRawText(result) {
|
|
3
|
+
if (typeof result === "string")
|
|
4
|
+
return result;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(result);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return String(result);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function summarizeArgs(toolName, args) {
|
|
13
|
+
switch (toolName) {
|
|
14
|
+
case "read_file":
|
|
15
|
+
case "write_file":
|
|
16
|
+
case "edit_file":
|
|
17
|
+
return String(args.path ?? "");
|
|
18
|
+
case "shell":
|
|
19
|
+
return String(args.command ?? "").slice(0, 80);
|
|
20
|
+
case "create_task":
|
|
21
|
+
return String(args.title ?? "");
|
|
22
|
+
case "complete_task":
|
|
23
|
+
case "hydrate":
|
|
24
|
+
case "soft_unload":
|
|
25
|
+
case "hard_unload":
|
|
26
|
+
return String(args.id ?? "").slice(0, 26);
|
|
27
|
+
case "add_constraint":
|
|
28
|
+
case "add_note":
|
|
29
|
+
return String(args.text ?? "").slice(0, 60);
|
|
30
|
+
case "decide":
|
|
31
|
+
return String(args.summary ?? "").slice(0, 60);
|
|
32
|
+
case "recall":
|
|
33
|
+
case "search_session_log":
|
|
34
|
+
return String(args.query ?? "").slice(0, 60);
|
|
35
|
+
case "remember":
|
|
36
|
+
return String(args.content ?? "").slice(0, 60);
|
|
37
|
+
default:
|
|
38
|
+
return Object.entries(args)
|
|
39
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v).slice(0, 40)}`)
|
|
40
|
+
.join(", ")
|
|
41
|
+
.slice(0, 80);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function summarizeResult(result) {
|
|
45
|
+
if (result === null || result === undefined)
|
|
46
|
+
return "done";
|
|
47
|
+
if (typeof result !== "object")
|
|
48
|
+
return String(result).slice(0, 120);
|
|
49
|
+
const r = result;
|
|
50
|
+
if (r.ok === false && r.error)
|
|
51
|
+
return `error: ${String(r.error).slice(0, 100)}`;
|
|
52
|
+
if (r.ok === true || r.ok === undefined) {
|
|
53
|
+
if (typeof r.stdout === "string" && r.stdout.length > 0) {
|
|
54
|
+
const lines = r.stdout.trim().split("\n").length;
|
|
55
|
+
return `exit ${r.exitCode ?? 0}, ${lines} line(s)`;
|
|
56
|
+
}
|
|
57
|
+
if (typeof r.content === "string") {
|
|
58
|
+
return `${r.content.length} chars`;
|
|
59
|
+
}
|
|
60
|
+
if (typeof r.output === "string") {
|
|
61
|
+
return r.output.slice(0, 100);
|
|
62
|
+
}
|
|
63
|
+
if (r.id)
|
|
64
|
+
return `id ${String(r.id).slice(0, 26)}`;
|
|
65
|
+
}
|
|
66
|
+
return JSON.stringify(result).slice(0, 100);
|
|
67
|
+
}
|