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.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/praana.js +17 -0
  4. package/bin/pran.js +17 -0
  5. package/dist/app-banner.d.ts +11 -0
  6. package/dist/app-banner.js +161 -0
  7. package/dist/app-controller.d.ts +44 -0
  8. package/dist/app-controller.js +143 -0
  9. package/dist/app-identity.d.ts +18 -0
  10. package/dist/app-identity.js +52 -0
  11. package/dist/auto-compact.d.ts +16 -0
  12. package/dist/auto-compact.js +101 -0
  13. package/dist/cli-args.d.ts +14 -0
  14. package/dist/cli-args.js +69 -0
  15. package/dist/compile-classic.d.ts +21 -0
  16. package/dist/compile-classic.js +106 -0
  17. package/dist/compiler.d.ts +75 -0
  18. package/dist/compiler.js +406 -0
  19. package/dist/config.d.ts +3 -0
  20. package/dist/config.js +433 -0
  21. package/dist/context-engine/activity-log.d.ts +9 -0
  22. package/dist/context-engine/activity-log.js +109 -0
  23. package/dist/context-engine/artifact-store.d.ts +32 -0
  24. package/dist/context-engine/artifact-store.js +272 -0
  25. package/dist/context-engine/bm25.d.ts +3 -0
  26. package/dist/context-engine/bm25.js +32 -0
  27. package/dist/context-engine/checkpoint.d.ts +34 -0
  28. package/dist/context-engine/checkpoint.js +430 -0
  29. package/dist/context-engine/classify.d.ts +3 -0
  30. package/dist/context-engine/classify.js +60 -0
  31. package/dist/context-engine/db.d.ts +73 -0
  32. package/dist/context-engine/db.js +505 -0
  33. package/dist/context-engine/distiller.d.ts +30 -0
  34. package/dist/context-engine/distiller.js +67 -0
  35. package/dist/context-engine/engine-compiler.d.ts +23 -0
  36. package/dist/context-engine/engine-compiler.js +297 -0
  37. package/dist/context-engine/error-tracker.d.ts +21 -0
  38. package/dist/context-engine/error-tracker.js +74 -0
  39. package/dist/context-engine/event-lineage.d.ts +26 -0
  40. package/dist/context-engine/event-lineage.js +120 -0
  41. package/dist/context-engine/extraction.d.ts +26 -0
  42. package/dist/context-engine/extraction.js +83 -0
  43. package/dist/context-engine/index.d.ts +82 -0
  44. package/dist/context-engine/index.js +238 -0
  45. package/dist/context-engine/scoring.d.ts +13 -0
  46. package/dist/context-engine/scoring.js +47 -0
  47. package/dist/context-engine/state-snapshot.d.ts +8 -0
  48. package/dist/context-engine/state-snapshot.js +50 -0
  49. package/dist/context-engine/summarize.d.ts +6 -0
  50. package/dist/context-engine/summarize.js +32 -0
  51. package/dist/context-engine/telemetry.d.ts +25 -0
  52. package/dist/context-engine/telemetry.js +64 -0
  53. package/dist/context-engine/turn-digest.d.ts +50 -0
  54. package/dist/context-engine/turn-digest.js +250 -0
  55. package/dist/context-engine/turn-ledger.d.ts +18 -0
  56. package/dist/context-engine/turn-ledger.js +184 -0
  57. package/dist/context-engine/turn-recorder.d.ts +24 -0
  58. package/dist/context-engine/turn-recorder.js +88 -0
  59. package/dist/context-engine/types.d.ts +201 -0
  60. package/dist/context-engine/types.js +4 -0
  61. package/dist/context-pressure.d.ts +19 -0
  62. package/dist/context-pressure.js +36 -0
  63. package/dist/distillers/generic.d.ts +14 -0
  64. package/dist/distillers/generic.js +93 -0
  65. package/dist/distillers/git-diff.d.ts +8 -0
  66. package/dist/distillers/git-diff.js +119 -0
  67. package/dist/distillers/index.d.ts +2 -0
  68. package/dist/distillers/index.js +16 -0
  69. package/dist/distillers/npm-test.d.ts +8 -0
  70. package/dist/distillers/npm-test.js +50 -0
  71. package/dist/distillers/rg-results.d.ts +8 -0
  72. package/dist/distillers/rg-results.js +28 -0
  73. package/dist/distillers/tsc-errors.d.ts +8 -0
  74. package/dist/distillers/tsc-errors.js +52 -0
  75. package/dist/event-log.d.ts +56 -0
  76. package/dist/event-log.js +214 -0
  77. package/dist/llm.d.ts +29 -0
  78. package/dist/llm.js +155 -0
  79. package/dist/logger.d.ts +94 -0
  80. package/dist/logger.js +287 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +54 -0
  83. package/dist/memory/confidence.d.ts +7 -0
  84. package/dist/memory/confidence.js +37 -0
  85. package/dist/memory/consolidation.d.ts +26 -0
  86. package/dist/memory/consolidation.js +166 -0
  87. package/dist/memory/db.d.ts +40 -0
  88. package/dist/memory/db.js +283 -0
  89. package/dist/memory/dedup.d.ts +6 -0
  90. package/dist/memory/dedup.js +50 -0
  91. package/dist/memory/embedder-factory.d.ts +3 -0
  92. package/dist/memory/embedder-factory.js +81 -0
  93. package/dist/memory/embeddings.d.ts +15 -0
  94. package/dist/memory/embeddings.js +67 -0
  95. package/dist/memory/index.d.ts +9 -0
  96. package/dist/memory/index.js +11 -0
  97. package/dist/memory/ollama-summarizer.d.ts +19 -0
  98. package/dist/memory/ollama-summarizer.js +72 -0
  99. package/dist/memory/openai-summarizer.d.ts +21 -0
  100. package/dist/memory/openai-summarizer.js +51 -0
  101. package/dist/memory/store.d.ts +61 -0
  102. package/dist/memory/store.js +502 -0
  103. package/dist/memory/summarizer-factory.d.ts +3 -0
  104. package/dist/memory/summarizer-factory.js +69 -0
  105. package/dist/memory/summarizer.d.ts +4 -0
  106. package/dist/memory/summarizer.js +112 -0
  107. package/dist/memory/types.d.ts +87 -0
  108. package/dist/memory/types.js +17 -0
  109. package/dist/model-context.d.ts +15 -0
  110. package/dist/model-context.js +212 -0
  111. package/dist/project-detector.d.ts +37 -0
  112. package/dist/project-detector.js +604 -0
  113. package/dist/render.d.ts +15 -0
  114. package/dist/render.js +46 -0
  115. package/dist/session.d.ts +118 -0
  116. package/dist/session.js +809 -0
  117. package/dist/skills/index.d.ts +69 -0
  118. package/dist/skills/index.js +885 -0
  119. package/dist/skills/types.d.ts +93 -0
  120. package/dist/skills/types.js +8 -0
  121. package/dist/slash-commands.d.ts +14 -0
  122. package/dist/slash-commands.js +301 -0
  123. package/dist/state-graph.d.ts +38 -0
  124. package/dist/state-graph.js +255 -0
  125. package/dist/status-bar.d.ts +54 -0
  126. package/dist/status-bar.js +184 -0
  127. package/dist/thinking-display.d.ts +21 -0
  128. package/dist/thinking-display.js +37 -0
  129. package/dist/tool-summary.d.ts +4 -0
  130. package/dist/tool-summary.js +67 -0
  131. package/dist/tools/index.d.ts +925 -0
  132. package/dist/tools/index.js +86 -0
  133. package/dist/tools/knowledge.d.ts +140 -0
  134. package/dist/tools/knowledge.js +260 -0
  135. package/dist/tools/memory.d.ts +39 -0
  136. package/dist/tools/memory.js +300 -0
  137. package/dist/tools/search-code.d.ts +134 -0
  138. package/dist/tools/search-code.js +390 -0
  139. package/dist/tools/system.d.ts +16 -0
  140. package/dist/tools/system.js +499 -0
  141. package/dist/tools/tool-def.d.ts +6 -0
  142. package/dist/tools/tool-def.js +3 -0
  143. package/dist/turn-control.d.ts +51 -0
  144. package/dist/turn-control.js +210 -0
  145. package/dist/turn.d.ts +20 -0
  146. package/dist/turn.js +624 -0
  147. package/dist/types.d.ts +233 -0
  148. package/dist/types.js +4 -0
  149. package/dist/ui/readline-ui.d.ts +2 -0
  150. package/dist/ui/readline-ui.js +176 -0
  151. package/dist/ui/tui/app.d.ts +13 -0
  152. package/dist/ui/tui/app.js +270 -0
  153. package/dist/ui/tui/busy-indicator.d.ts +2 -0
  154. package/dist/ui/tui/busy-indicator.js +13 -0
  155. package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
  156. package/dist/ui/tui/components/gutter-rule.js +9 -0
  157. package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
  158. package/dist/ui/tui/components/inline-tool-row.js +8 -0
  159. package/dist/ui/tui/components/prompt-input.d.ts +20 -0
  160. package/dist/ui/tui/components/prompt-input.js +120 -0
  161. package/dist/ui/tui/components/system-line.d.ts +5 -0
  162. package/dist/ui/tui/components/system-line.js +6 -0
  163. package/dist/ui/tui/components/thinking-block.d.ts +11 -0
  164. package/dist/ui/tui/components/thinking-block.js +31 -0
  165. package/dist/ui/tui/components/toast-line.d.ts +4 -0
  166. package/dist/ui/tui/components/toast-line.js +8 -0
  167. package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
  168. package/dist/ui/tui/components/tool-result-line.js +6 -0
  169. package/dist/ui/tui/components/turn-footer.d.ts +5 -0
  170. package/dist/ui/tui/components/turn-footer.js +7 -0
  171. package/dist/ui/tui/components/user-block.d.ts +6 -0
  172. package/dist/ui/tui/components/user-block.js +6 -0
  173. package/dist/ui/tui/logo-banner.d.ts +5 -0
  174. package/dist/ui/tui/logo-banner.js +8 -0
  175. package/dist/ui/tui/markdown-render.d.ts +16 -0
  176. package/dist/ui/tui/markdown-render.js +218 -0
  177. package/dist/ui/tui/palette.d.ts +12 -0
  178. package/dist/ui/tui/palette.js +13 -0
  179. package/dist/ui/tui/reasoning-summary.d.ts +12 -0
  180. package/dist/ui/tui/reasoning-summary.js +27 -0
  181. package/dist/ui/tui/reducer.d.ts +92 -0
  182. package/dist/ui/tui/reducer.js +260 -0
  183. package/dist/ui/tui/run.d.ts +3 -0
  184. package/dist/ui/tui/run.js +40 -0
  185. package/dist/ui/tui/sink.d.ts +4 -0
  186. package/dist/ui/tui/sink.js +89 -0
  187. package/dist/ui/tui/status-bar-view.d.ts +5 -0
  188. package/dist/ui/tui/status-bar-view.js +44 -0
  189. package/dist/ui/tui/terminal-height.d.ts +12 -0
  190. package/dist/ui/tui/terminal-height.js +20 -0
  191. package/dist/ui/tui/terminal-width.d.ts +2 -0
  192. package/dist/ui/tui/terminal-width.js +5 -0
  193. package/dist/ui/tui/tool-display.d.ts +23 -0
  194. package/dist/ui/tui/tool-display.js +217 -0
  195. package/dist/ui/tui/transcript-line.d.ts +12 -0
  196. package/dist/ui/tui/transcript-line.js +43 -0
  197. package/dist/ui/tui/transcript-replay.d.ts +12 -0
  198. package/dist/ui/tui/transcript-replay.js +117 -0
  199. package/dist/ui-events.d.ts +39 -0
  200. package/dist/ui-events.js +33 -0
  201. package/dist/ui.d.ts +77 -0
  202. package/dist/ui.js +179 -0
  203. package/package.json +73 -0
  204. package/praana.config.example.toml +231 -0
@@ -0,0 +1,4 @@
1
+ import type { Dispatch } from "react";
2
+ import type { TurnUiSink } from "../../ui-events.js";
3
+ import type { TranscriptAction } from "./reducer.js";
4
+ export declare function createTuiTurnSink(dispatch: Dispatch<TranscriptAction>): TurnUiSink;
@@ -0,0 +1,89 @@
1
+ const THROTTLE_MS = 50;
2
+ /**
3
+ * Create a throttled text delta dispatcher.
4
+ *
5
+ * Rapid token-by-token streaming causes Ink to re-render the entire component
6
+ * tree on every dispatch, which triggers terminal redraws and a strobing effect.
7
+ * This buffer coalesces deltas within a time window so the terminal only updates
8
+ * at a controlled rate (~20 fps).
9
+ */
10
+ function createThrottledDispatcher(dispatch) {
11
+ let buffer = "";
12
+ let timer = null;
13
+ const flushBuffer = () => {
14
+ if (buffer) {
15
+ dispatch({ type: "assistant_delta", delta: buffer });
16
+ buffer = "";
17
+ }
18
+ timer = null;
19
+ };
20
+ const onDelta = (delta) => {
21
+ buffer += delta;
22
+ if (!timer) {
23
+ timer = setTimeout(flushBuffer, THROTTLE_MS);
24
+ }
25
+ };
26
+ return { onDelta, flush: flushBuffer };
27
+ }
28
+ export function createTuiTurnSink(dispatch) {
29
+ const text = createThrottledDispatcher(dispatch);
30
+ let pendingStats = null;
31
+ return {
32
+ onTextDelta: (delta) => text.onDelta(delta),
33
+ onThinkingDelta: (delta) => {
34
+ dispatch({ type: "thinking_delta", delta });
35
+ },
36
+ onToolCallsStart: () => {
37
+ text.flush();
38
+ dispatch({ type: "thinking_close" });
39
+ },
40
+ onToolCall: (toolName, args) => {
41
+ text.flush();
42
+ dispatch({ type: "tool_call", toolName, args });
43
+ },
44
+ onToolResult: (toolName, resultText, isError) => {
45
+ text.flush();
46
+ dispatch({ type: "tool_result", toolName, resultText, isError });
47
+ },
48
+ flushText: () => text.flush(),
49
+ consumeTurnStats: () => {
50
+ const stats = pendingStats;
51
+ pendingStats = null;
52
+ return stats;
53
+ },
54
+ onDebug: (message) => {
55
+ dispatch({ type: "system_lines", lines: [`[debug] ${message}`] });
56
+ },
57
+ onDebugBlock: (stepIndex, toolCalls, toolResults) => {
58
+ const lines = [`[debug] step ${stepIndex} tools`];
59
+ for (const tc of toolCalls) {
60
+ lines.push(` > ${tc.toolName}(${JSON.stringify(tc.args).slice(0, 120)})`);
61
+ }
62
+ for (const tr of toolResults) {
63
+ lines.push(` < ${tr.toolName}`);
64
+ }
65
+ dispatch({ type: "system_lines", lines });
66
+ },
67
+ onMemoryBanner: (stats) => {
68
+ pendingStats = stats;
69
+ },
70
+ onSpinnerStart: () => {
71
+ /* busy state handled by turn wrapper */
72
+ },
73
+ onSpinnerStop: () => {
74
+ /* noop */
75
+ },
76
+ onNewline: () => {
77
+ dispatch({ type: "assistant_delta", delta: "\n" });
78
+ },
79
+ onFallback: (text) => {
80
+ dispatch({ type: "assistant_delta", delta: text + "\n" });
81
+ },
82
+ onError: (entry) => {
83
+ dispatch({
84
+ type: "system_lines",
85
+ lines: [`[${entry.level}] ${entry.domain}: ${entry.message}`],
86
+ });
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import type { StatusBarInput } from "../../status-bar.js";
3
+ export declare function StatusBarView({ status, }: {
4
+ status: StatusBarInput;
5
+ }): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { formatTokenCount } from "../../status-bar.js";
4
+ import { PALETTE } from "./palette.js";
5
+ import { getTerminalWidth } from "./terminal-width.js";
6
+ function formatStateLabel(stats) {
7
+ const parts = [];
8
+ if (stats.active > 0)
9
+ parts.push(`${stats.active} active`);
10
+ if (stats.soft > 0)
11
+ parts.push(`${stats.soft} soft`);
12
+ if (stats.hard > 0)
13
+ parts.push(`${stats.hard} hard`);
14
+ return parts.join(" · ");
15
+ }
16
+ function formatMemLabel(status) {
17
+ if (status.incognito)
18
+ return "incognito";
19
+ if (!status.memoryEnabled)
20
+ return "off (config)";
21
+ return "on";
22
+ }
23
+ function truncate(text, max) {
24
+ if (text.length <= max)
25
+ return text;
26
+ return `${text.slice(0, max - 1)}…`;
27
+ }
28
+ export function StatusBarView({ status, }) {
29
+ const { memoryStats, currentTask, skills } = status;
30
+ const width = getTerminalWidth();
31
+ const pct = status.contextWindowTokens > 0
32
+ ? Math.min(100, Math.round((status.contextUsedTokens / status.contextWindowTokens) * 100))
33
+ : 0;
34
+ const modelShort = status.model.split("/").pop() ?? status.model;
35
+ const memStr = formatMemLabel(status);
36
+ const skillsCount = skills.length;
37
+ const stateStr = formatStateLabel(memoryStats);
38
+ const ctxStr = status.contextWindowTokens > 0
39
+ ? `${formatTokenCount(status.contextUsedTokens)}/${formatTokenCount(status.contextWindowTokens)} (${pct}%)`
40
+ : `${pct}%`;
41
+ const thinkStr = status.thinking ? "on" : "off";
42
+ const taskLabel = currentTask ? truncate(currentTask, Math.max(24, width - 20)) : null;
43
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: PALETTE.assistant, children: `📦 ${modelShort}` }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: pct > 90 ? PALETTE.error : pct > 70 ? PALETTE.tool : PALETTE.muted, children: `🧠 ctx ${ctxStr}` }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: status.thinking ? PALETTE.tool : PALETTE.muted, children: `💭 think ${thinkStr}` }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: memStr === "on" ? PALETTE.user : PALETTE.muted, children: `💾 mem ${memStr}` }), skillsCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: PALETTE.tool, children: `🛠️ ${skillsCount} skills` })] })), stateStr && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: PALETTE.muted, children: `◇ ${stateStr}` })] }))] }), taskLabel ? (_jsx(Box, { marginTop: 0, children: _jsx(Text, { color: PALETTE.muted, dimColor: true, children: `🎯 ${taskLabel}` }) })) : null] }));
44
+ }
@@ -0,0 +1,12 @@
1
+ /** Best-effort terminal row count for layout (TUI). */
2
+ export declare function getTerminalRows(): number;
3
+ export interface TranscriptBudgetOptions {
4
+ showLogo?: boolean;
5
+ showToast?: boolean;
6
+ showScrollHint?: boolean;
7
+ }
8
+ /**
9
+ * Lines available for transcript content after reserving chrome
10
+ * (prompt, status bar, padding, optional logo/toast).
11
+ */
12
+ export declare function getTranscriptLineBudget(options?: TranscriptBudgetOptions): number;
@@ -0,0 +1,20 @@
1
+ /** Best-effort terminal row count for layout (TUI). */
2
+ export function getTerminalRows() {
3
+ const rows = process.stdout.rows ?? process.stderr.rows ?? 24;
4
+ return Math.max(12, Math.min(rows, 200));
5
+ }
6
+ /**
7
+ * Lines available for transcript content after reserving chrome
8
+ * (prompt, status bar, padding, optional logo/toast).
9
+ */
10
+ export function getTranscriptLineBudget(options = {}) {
11
+ const rows = getTerminalRows();
12
+ let overhead = 7; // outer padding, prompt row, status bar, gaps
13
+ if (options.showLogo)
14
+ overhead += 10;
15
+ if (options.showToast)
16
+ overhead += 1;
17
+ if (options.showScrollHint)
18
+ overhead += 2;
19
+ return Math.max(6, rows - overhead);
20
+ }
@@ -0,0 +1,2 @@
1
+ /** Best-effort terminal width for layout (TUI). */
2
+ export declare function getTerminalWidth(): number;
@@ -0,0 +1,5 @@
1
+ /** Best-effort terminal width for layout (TUI). */
2
+ export function getTerminalWidth() {
3
+ const cols = process.stdout.columns ?? process.stderr.columns ?? 80;
4
+ return Math.max(40, Math.min(cols, 200));
5
+ }
@@ -0,0 +1,23 @@
1
+ import type { MemoryBannerStats } from "../../ui-events.js";
2
+ export interface ToolDisplayInfo {
3
+ icon: string;
4
+ label: string;
5
+ pending: string;
6
+ }
7
+ /** Produce a compact one-line summary of a tool result for display. */
8
+ export declare function summarizeResultForDisplay(text: string): string;
9
+ export declare function formatToolDisplay(toolName: string, args: Record<string, unknown>): ToolDisplayInfo;
10
+ /** Turn-specific stats for the footer (excludes state — tier counts live in the status bar). */
11
+ export declare function formatTurnStatsSuffix(stats?: MemoryBannerStats): string;
12
+ export declare function formatTurnFooter(model: string, durationMs: number, stats?: MemoryBannerStats): string;
13
+ /** Compact boot summary for the TUI welcome panel. */
14
+ export declare function formatTuiBootSummary(input: {
15
+ sessionId: string;
16
+ contextTokens?: number;
17
+ engineEnabled: boolean;
18
+ skillCount: number;
19
+ memoryEnabled: boolean;
20
+ incognito: boolean;
21
+ }): string;
22
+ /** Whether this entry should have extra top margin (visual break from prior block). */
23
+ export declare function needsTopMargin(role: string, prevRole: string | undefined): boolean;
@@ -0,0 +1,217 @@
1
+ import { summarizeArgs } from "../../tool-summary.js";
2
+ function fmtToken(n) {
3
+ if (n < 1000)
4
+ return String(n);
5
+ if (n < 1_000_000)
6
+ return `${(n / 1000).toFixed(1)}k`;
7
+ return `${(n / 1_000_000).toFixed(1)}M`;
8
+ }
9
+ function shellShortLabel(command) {
10
+ const trimmed = command.trim();
11
+ if (trimmed.length <= 64)
12
+ return trimmed;
13
+ const segments = trimmed.split("&&").map((s) => s.trim());
14
+ const last = segments[segments.length - 1] ?? trimmed;
15
+ const short = last.split(/\s+/).slice(0, 4).join(" ");
16
+ return short.length > 56 ? `${short.slice(0, 55)}…` : short;
17
+ }
18
+ /** Produce a compact one-line summary of a tool result for display. */
19
+ export function summarizeResultForDisplay(text) {
20
+ if (!text)
21
+ return "(empty)";
22
+ const artifactMatch = text.match(/\[artifact:\s*(art_[a-f0-9]+)/i);
23
+ if (artifactMatch) {
24
+ const tokenMatch = text.match(/([\d,]+)\s*tokens?\b/i);
25
+ const id = artifactMatch[1];
26
+ return tokenMatch
27
+ ? `artifact ${id.slice(0, 12)}… · ${tokenMatch[1].replace(/,/g, "")} tokens`
28
+ : `artifact ${id.slice(0, 16)}…`;
29
+ }
30
+ try {
31
+ const parsed = JSON.parse(text);
32
+ if (parsed.ok === false) {
33
+ return `error: ${String(parsed.error ?? "failed").slice(0, 72)}`;
34
+ }
35
+ if (typeof parsed.stdout === "string") {
36
+ const stdout = parsed.stdout.trim();
37
+ const lineCount = stdout ? stdout.split("\n").filter(Boolean).length : 0;
38
+ const preview = stdout.split("\n")[0]?.slice(0, 56) ?? "";
39
+ const suffix = preview ? ` — ${preview}${stdout.length > 56 ? "…" : ""}` : "";
40
+ return `exit ${parsed.exitCode ?? 0} · ${lineCount} line(s)${suffix}`;
41
+ }
42
+ if (typeof parsed.content === "string") {
43
+ const content = parsed.content;
44
+ const lineCount = content.split("\n").length;
45
+ const preview = content.split("\n")[0]?.slice(0, 56) ?? "";
46
+ return `${lineCount} line(s) · ${preview}${content.length > 56 ? "…" : ""}`;
47
+ }
48
+ if (parsed.id) {
49
+ return `ok · ${String(parsed.id).slice(0, 28)}`;
50
+ }
51
+ }
52
+ catch {
53
+ /* plain text */
54
+ }
55
+ const lines = text.split("\n").length;
56
+ const chars = text.length;
57
+ const preview = text.slice(0, 200).split("\n")[0];
58
+ const previewText = preview.length > 72 ? `${preview.slice(0, 71)}…` : preview;
59
+ const size = lines > 1 ? `${lines} lines, ${chars} chars` : `${chars} chars`;
60
+ return `${size} — ${previewText}`;
61
+ }
62
+ function formatPath(path) {
63
+ const value = String(path ?? "");
64
+ if (!value)
65
+ return "";
66
+ return value.length > 60 ? "…" + value.slice(-57) : value;
67
+ }
68
+ export function formatToolDisplay(toolName, args) {
69
+ switch (toolName) {
70
+ case "shell": {
71
+ const command = String(args.command ?? "");
72
+ return {
73
+ icon: "$",
74
+ label: command ? shellShortLabel(command) : "shell",
75
+ pending: "Running command…",
76
+ };
77
+ }
78
+ case "retrieve_artifact": {
79
+ const id = String(args.id ?? args.artifact_id ?? "").slice(0, 16);
80
+ return {
81
+ icon: "◆",
82
+ label: id ? `Artifact ${id}` : "Retrieve artifact",
83
+ pending: "Retrieving artifact…",
84
+ };
85
+ }
86
+ case "read_file": {
87
+ const path = formatPath(args.path);
88
+ return { icon: "→", label: `Read ${path}`, pending: "Reading file…" };
89
+ }
90
+ case "write_file": {
91
+ const path = formatPath(args.path);
92
+ return { icon: "←", label: `Write ${path}`, pending: "Writing file…" };
93
+ }
94
+ case "edit_file": {
95
+ const path = formatPath(args.path);
96
+ return { icon: "✎", label: `Edit ${path}`, pending: "Editing file…" };
97
+ }
98
+ case "search_code": {
99
+ const pattern = String(args.pattern ?? "");
100
+ const path = args.path ? ` in ${formatPath(args.path)}` : "";
101
+ return {
102
+ icon: "✱",
103
+ label: `Grep "${pattern}"${path}`,
104
+ pending: "Searching…",
105
+ };
106
+ }
107
+ case "recall": {
108
+ const query = String(args.query ?? "").slice(0, 80);
109
+ return { icon: "◈", label: `Recall "${query}"`, pending: "Recalling…" };
110
+ }
111
+ case "search_session_log": {
112
+ const query = String(args.query ?? "").slice(0, 80);
113
+ return { icon: "◈", label: `Search log "${query}"`, pending: "Searching log…" };
114
+ }
115
+ case "create_task": {
116
+ const title = String(args.title ?? "").slice(0, 80);
117
+ return { icon: "◇", label: `Task ${title}`, pending: "Creating task…" };
118
+ }
119
+ case "complete_task":
120
+ case "hydrate":
121
+ case "soft_unload":
122
+ case "hard_unload": {
123
+ const id = String(args.id ?? "").slice(0, 26);
124
+ return { icon: "◇", label: `${toolName} ${id}`, pending: "Updating state…" };
125
+ }
126
+ case "remember": {
127
+ const content = String(args.content ?? "").slice(0, 60);
128
+ return { icon: "◈", label: `Remember ${content}`, pending: "Storing memory…" };
129
+ }
130
+ default: {
131
+ const summary = summarizeArgs(toolName, args);
132
+ return {
133
+ icon: "⚙",
134
+ label: summary ? `${toolName} ${summary}` : toolName,
135
+ pending: "Running…",
136
+ };
137
+ }
138
+ }
139
+ }
140
+ /** Turn-specific stats for the footer (excludes state — tier counts live in the status bar). */
141
+ export function formatTurnStatsSuffix(stats) {
142
+ if (!stats)
143
+ return "";
144
+ const parts = [];
145
+ if (stats.promptTokens > 0)
146
+ parts.push(`prompt ~${fmtToken(stats.promptTokens)}`);
147
+ if (stats.outputTokens > 0)
148
+ parts.push(`out ~${fmtToken(stats.outputTokens)}`);
149
+ if (stats.digestLen > 0)
150
+ parts.push(`digest ${stats.digestLen}c`);
151
+ if (stats.recallCalls > 0) {
152
+ parts.push(`recall ${stats.recallHits}/${stats.recallCalls}`);
153
+ }
154
+ if (stats.autoHydrated > 0)
155
+ parts.push(`auto+${stats.autoHydrated}`);
156
+ return parts.join(" · ");
157
+ }
158
+ export function formatTurnFooter(model, durationMs, stats) {
159
+ const modelShort = model.split("/").pop() ?? model;
160
+ const duration = durationMs < 1000
161
+ ? `${Math.max(0, Math.round(durationMs))}ms`
162
+ : `${(durationMs / 1000).toFixed(1)}s`;
163
+ const parts = [`▣ PRAANA`, modelShort, duration];
164
+ const suffix = formatTurnStatsSuffix(stats);
165
+ if (suffix)
166
+ parts.push(suffix);
167
+ return parts.join(" · ");
168
+ }
169
+ /** Compact boot summary for the TUI welcome panel. */
170
+ export function formatTuiBootSummary(input) {
171
+ const parts = [`session ${input.sessionId}`];
172
+ if (input.contextTokens && input.contextTokens > 0) {
173
+ parts.push(`ctx ~${input.contextTokens} tok`);
174
+ }
175
+ if (input.engineEnabled)
176
+ parts.push("engine on");
177
+ if (input.skillCount > 0) {
178
+ parts.push(`${input.skillCount} skill${input.skillCount === 1 ? "" : "s"}`);
179
+ }
180
+ if (input.incognito)
181
+ parts.push("incognito");
182
+ else if (!input.memoryEnabled) {
183
+ parts.push("mem off (enable memory.enabled in config)");
184
+ }
185
+ return parts.join(" · ");
186
+ }
187
+ const BLOCK_ROLES = new Set([
188
+ "user",
189
+ "assistant",
190
+ "tool",
191
+ "tool_result",
192
+ "thinking",
193
+ "turn_footer",
194
+ "system",
195
+ ]);
196
+ /** Whether this entry should have extra top margin (visual break from prior block). */
197
+ export function needsTopMargin(role, prevRole) {
198
+ if (role === "user")
199
+ return true;
200
+ if (!prevRole)
201
+ return false;
202
+ if (role === prevRole)
203
+ return false;
204
+ if (role === "turn_footer")
205
+ return true;
206
+ if (role === "thinking" && prevRole !== "thinking")
207
+ return true;
208
+ if (role === "tool" &&
209
+ prevRole !== "tool" &&
210
+ prevRole !== "tool_result") {
211
+ return true;
212
+ }
213
+ if (role === "assistant" && BLOCK_ROLES.has(prevRole)) {
214
+ return true;
215
+ }
216
+ return false;
217
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import type { TranscriptEntry } from "./reducer.js";
3
+ export interface TranscriptLineProps {
4
+ entry: TranscriptEntry;
5
+ prevRole?: TranscriptEntry["role"];
6
+ live?: boolean;
7
+ showThinking?: boolean;
8
+ markdownRendering?: boolean;
9
+ syntaxHighlighting?: boolean;
10
+ syntaxTheme?: string;
11
+ }
12
+ export declare const TranscriptLine: React.NamedExoticComponent<TranscriptLineProps>;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import stripAnsi from "strip-ansi";
5
+ import { PALETTE } from "./palette.js";
6
+ import { MarkdownRender } from "./markdown-render.js";
7
+ import { needsTopMargin } from "./tool-display.js";
8
+ import { UserBlock } from "./components/user-block.js";
9
+ import { ThinkingBlock } from "./components/thinking-block.js";
10
+ import { InlineToolRow } from "./components/inline-tool-row.js";
11
+ import { ToolResultLine } from "./components/tool-result-line.js";
12
+ import { TurnFooter } from "./components/turn-footer.js";
13
+ import { SystemLine } from "./components/system-line.js";
14
+ export const TranscriptLine = React.memo(function TranscriptLine({ entry, prevRole, live = false, showThinking = false, markdownRendering = true, syntaxHighlighting = true, syntaxTheme = "solarized-dark", }) {
15
+ const plain = stripAnsi(entry.text);
16
+ const marginTop = needsTopMargin(entry.role, prevRole);
17
+ if (entry.role === "user") {
18
+ return (_jsx(UserBlock, { text: plain, marginTop: marginTop, showTurnBreak: prevRole === "turn_footer" }));
19
+ }
20
+ if (entry.role === "thinking") {
21
+ return (_jsx(ThinkingBlock, { text: plain, live: live, showBody: showThinking, durationMs: entry.durationMs, markdownRendering: markdownRendering, syntaxHighlighting: syntaxHighlighting, syntaxTheme: syntaxTheme }));
22
+ }
23
+ if (entry.role === "tool") {
24
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(InlineToolRow, { icon: entry.toolIcon ?? "⚙", label: entry.toolLabel ?? plain, pending: entry.toolPending, complete: entry.resultSummary !== undefined, isError: entry.isError, marginTop: marginTop }), entry.resultSummary ? (_jsx(ToolResultLine, { summary: entry.resultSummary, isError: entry.isError })) : null] }));
25
+ }
26
+ if (entry.role === "tool_result") {
27
+ const summary = entry.resultSummary ?? plain;
28
+ return (_jsx(Box, { flexDirection: "column", marginTop: marginTop ? 1 : 0, paddingLeft: 1, children: _jsx(ToolResultLine, { summary: summary, isError: entry.isError }) }));
29
+ }
30
+ if (entry.role === "turn_footer") {
31
+ return _jsx(TurnFooter, { text: plain, marginTop: true });
32
+ }
33
+ if (entry.role === "system") {
34
+ return _jsx(SystemLine, { text: plain, marginTop: marginTop });
35
+ }
36
+ if (entry.role === "assistant") {
37
+ if (!plain.trim())
38
+ return null;
39
+ const useMarkdown = markdownRendering;
40
+ return (_jsx(Box, { flexDirection: "column", marginTop: marginTop ? 1 : 0, paddingLeft: 1, children: useMarkdown ? (_jsx(MarkdownRender, { text: plain, syntaxHighlighting: syntaxHighlighting, syntaxTheme: syntaxTheme })) : (_jsx(Text, { wrap: "wrap", color: PALETTE.text, children: plain || " " })) }));
41
+ }
42
+ return (_jsx(Box, { marginTop: marginTop ? 1 : 0, paddingLeft: 1, children: _jsx(Text, { wrap: "wrap", children: plain || " " }) }));
43
+ });
@@ -0,0 +1,12 @@
1
+ import type { Event } from "../../types.js";
2
+ import type { TranscriptEntry } from "./reducer.js";
3
+ /** Rebuild TUI transcript entries from session event log (resume). */
4
+ export declare function buildTranscriptFromEvents(events: Event[]): TranscriptEntry[];
5
+ /** Rough line budget for scroll windowing (terminals are line-limited). */
6
+ export declare function estimateEntryLines(entry: TranscriptEntry, terminalWidth?: number): number;
7
+ export declare function sliceEntriesByLineBudget(entries: TranscriptEntry[], lineBudget: number, scrollOffsetLines: number, terminalWidth?: number): {
8
+ entries: TranscriptEntry[];
9
+ startIndex: number;
10
+ totalLines: number;
11
+ maxScrollLines: number;
12
+ };
@@ -0,0 +1,117 @@
1
+ import { formatToolDisplay, summarizeResultForDisplay, } from "./tool-display.js";
2
+ import { formatToolResultRawText } from "../../tool-summary.js";
3
+ /** Rebuild TUI transcript entries from session event log (resume). */
4
+ export function buildTranscriptFromEvents(events) {
5
+ const entries = [];
6
+ let nextId = 1;
7
+ let groupCounter = 0;
8
+ const push = (entry) => {
9
+ entries.push({ ...entry, id: `replay-${nextId++}`, group: groupCounter });
10
+ };
11
+ for (const ev of events) {
12
+ switch (ev.kind) {
13
+ case "user_message": {
14
+ groupCounter++;
15
+ const text = String(ev.payload.text ?? "").trim();
16
+ if (text)
17
+ push({ role: "user", text });
18
+ break;
19
+ }
20
+ case "tool_call": {
21
+ const toolName = String(ev.payload.tool ?? "tool");
22
+ const args = (ev.payload.args ?? {});
23
+ const display = formatToolDisplay(toolName, args);
24
+ push({
25
+ role: "tool",
26
+ toolName,
27
+ text: toolName,
28
+ toolIcon: display.icon,
29
+ toolLabel: display.label,
30
+ toolPending: display.pending,
31
+ });
32
+ break;
33
+ }
34
+ case "tool_result": {
35
+ const toolName = String(ev.payload.tool ?? "tool");
36
+ const raw = formatToolResultRawText(ev.payload.result);
37
+ const summary = summarizeResultForDisplay(raw);
38
+ const isError = typeof ev.payload.result === "object" &&
39
+ ev.payload.result !== null &&
40
+ ev.payload.result.ok === false;
41
+ for (let i = entries.length - 1; i >= 0; i--) {
42
+ const entry = entries[i];
43
+ if (entry?.role === "tool" &&
44
+ entry.toolName === toolName &&
45
+ entry.resultSummary === undefined) {
46
+ entries[i] = {
47
+ ...entry,
48
+ resultSummary: summary,
49
+ resultText: raw,
50
+ isError: isError ?? false,
51
+ };
52
+ break;
53
+ }
54
+ }
55
+ break;
56
+ }
57
+ case "agent_message": {
58
+ const text = String(ev.payload.text ?? "").trim();
59
+ if (text)
60
+ push({ role: "assistant", text });
61
+ break;
62
+ }
63
+ default:
64
+ break;
65
+ }
66
+ }
67
+ return entries;
68
+ }
69
+ /** Rough line budget for scroll windowing (terminals are line-limited). */
70
+ export function estimateEntryLines(entry, terminalWidth = 80) {
71
+ const lines = entry.text.split("\n").length;
72
+ const wrapCols = Math.max(40, terminalWidth - 4);
73
+ switch (entry.role) {
74
+ case "user":
75
+ return Math.max(4, lines + 3);
76
+ case "assistant":
77
+ return Math.max(2, lines + Math.ceil(entry.text.length / wrapCols));
78
+ case "tool":
79
+ return entry.resultSummary ? 4 : 2;
80
+ case "thinking":
81
+ return 3;
82
+ case "turn_footer":
83
+ return 3;
84
+ case "system":
85
+ return Math.max(1, lines);
86
+ default:
87
+ return Math.max(1, lines);
88
+ }
89
+ }
90
+ export function sliceEntriesByLineBudget(entries, lineBudget, scrollOffsetLines, terminalWidth = 80) {
91
+ const weights = entries.map((entry) => estimateEntryLines(entry, terminalWidth));
92
+ const totalLines = weights.reduce((a, b) => a + b, 0);
93
+ const maxScrollLines = Math.max(0, totalLines - lineBudget);
94
+ const offset = Math.min(Math.max(0, scrollOffsetLines), maxScrollLines);
95
+ if (totalLines <= lineBudget) {
96
+ return { entries, startIndex: 0, totalLines, maxScrollLines: 0 };
97
+ }
98
+ const topLine = Math.max(0, totalLines - lineBudget - offset);
99
+ let start = 0;
100
+ let lineAcc = 0;
101
+ while (start < entries.length && lineAcc + (weights[start] ?? 1) <= topLine) {
102
+ lineAcc += weights[start] ?? 1;
103
+ start++;
104
+ }
105
+ let end = start;
106
+ let windowLines = 0;
107
+ while (end < entries.length && windowLines < lineBudget) {
108
+ windowLines += weights[end] ?? 1;
109
+ end++;
110
+ }
111
+ return {
112
+ entries: entries.slice(start, end),
113
+ startIndex: start,
114
+ totalLines,
115
+ maxScrollLines,
116
+ };
117
+ }
@@ -0,0 +1,39 @@
1
+ import type { LogEntry } from "./logger.js";
2
+ import type { computeMemoryStats } from "./turn.js";
3
+ export type MemoryBannerStats = ReturnType<typeof computeMemoryStats>;
4
+ /** UI sink for turn execution — replaces direct stdout/stderr writes when provided. */
5
+ export interface TurnUiSink {
6
+ onTextDelta?(delta: string): void;
7
+ onThinkingDelta?(delta: string): void;
8
+ onToolCallsStart?(): void;
9
+ onToolCall?(toolName: string, args: Record<string, unknown>): void;
10
+ /** Notify UI of the raw tool result text for rendering as a distinct block. */
11
+ onToolResult?(toolName: string, resultText: string, isError?: boolean): void;
12
+ onDebug?(message: string): void;
13
+ onDebugBlock?(stepIndex: number, toolCalls: Array<{
14
+ toolName: string;
15
+ args: Record<string, unknown>;
16
+ }>, toolResults: Array<{
17
+ toolName: string;
18
+ result: unknown;
19
+ }>): void;
20
+ onMemoryBanner?(stats: MemoryBannerStats): void;
21
+ onSpinnerStart?(text: string): void;
22
+ onSpinnerStop?(): void;
23
+ onNewline?(): void;
24
+ onFallback?(text: string): void;
25
+ /** Structured error for UI display (LLM failures, etc.). */
26
+ onError?(entry: LogEntry): void;
27
+ /** Flush any buffered text before dispatching terminal actions (e.g. assistant_complete).
28
+ * Used by throttled sinks to ensure no text is lost. */
29
+ flushText?(): void;
30
+ /** Take buffered turn stats for the combined turn footer line. */
31
+ consumeTurnStats?(): MemoryBannerStats | null;
32
+ }
33
+ /** Default sink: streaming callbacks + legacy terminal helpers. */
34
+ export declare function createDefaultTurnSink(options?: {
35
+ onTextDelta?: (delta: string) => void;
36
+ onThinkingDelta?: (delta: string) => void;
37
+ onToolCallsStart?: () => void;
38
+ }): TurnUiSink;
39
+ export declare function hasTurnUiSink(sink?: TurnUiSink): sink is TurnUiSink;