@herbertgao/pi-extensions 2026.8.12 → 2026.8.13

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 (112) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +376 -0
  4. package/node_modules/@luxusai/pi-hindsight/README.md +92 -0
  5. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +56 -0
  6. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +169 -0
  7. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +107 -0
  8. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +89 -0
  9. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +189 -0
  10. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  11. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  12. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +54 -0
  13. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +75 -0
  14. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +300 -0
  15. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +158 -0
  16. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +164 -0
  17. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +139 -0
  18. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +74 -0
  19. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +261 -0
  20. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +151 -0
  21. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +18 -0
  22. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +46 -0
  23. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +383 -0
  24. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +240 -0
  25. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +176 -0
  26. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +178 -0
  27. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +41 -0
  28. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +386 -0
  29. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +39 -0
  30. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +34 -0
  31. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +140 -0
  32. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +61 -0
  33. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +925 -0
  34. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +247 -0
  35. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +547 -0
  36. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +480 -0
  37. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +182 -0
  38. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +86 -0
  39. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +1010 -0
  40. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +175 -0
  41. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +425 -0
  42. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +210 -0
  43. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +817 -0
  44. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +25 -0
  45. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +319 -0
  46. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +196 -0
  47. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +189 -0
  48. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +65 -0
  49. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +260 -0
  50. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +244 -0
  51. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +59 -0
  52. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +82 -0
  53. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +45 -0
  54. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +331 -0
  55. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +354 -0
  56. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +66 -0
  57. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +132 -0
  58. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +228 -0
  59. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +74 -0
  60. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +16 -0
  61. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +648 -0
  62. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +105 -0
  63. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +68 -0
  64. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +71 -0
  65. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +13 -0
  66. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +162 -0
  67. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +131 -0
  68. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +104 -0
  69. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +37 -0
  70. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +893 -0
  71. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +35 -0
  72. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +177 -0
  73. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +7 -0
  74. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +21 -0
  75. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +112 -0
  76. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +111 -0
  77. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +214 -0
  78. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +70 -0
  79. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +392 -0
  80. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +45 -0
  81. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +9 -0
  82. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +860 -0
  83. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +105 -0
  84. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +189 -0
  85. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +299 -0
  86. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +201 -0
  87. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +47 -0
  88. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +239 -0
  89. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +50 -0
  90. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +259 -0
  91. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +77 -0
  92. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +534 -0
  93. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +319 -0
  94. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +325 -0
  95. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +42 -0
  96. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +244 -0
  97. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +56 -0
  98. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +50 -0
  99. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +167 -0
  100. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +207 -0
  101. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +120 -0
  102. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +8 -0
  103. package/node_modules/@luxusai/pi-hindsight/package.json +118 -0
  104. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +75 -0
  105. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  106. package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
  107. package/node_modules/pi-mcp-adapter/config.ts +10 -0
  108. package/node_modules/pi-mcp-adapter/dist/config.js +10 -0
  109. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  110. package/node_modules/pi-mcp-adapter/init.ts +5 -1
  111. package/node_modules/pi-mcp-adapter/package.json +1 -1
  112. package/package.json +10 -4
@@ -0,0 +1,201 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ResolvedConfig } from "../types.js";
3
+ import {
4
+ buildConfigEditingFields,
5
+ inputDefaultForConfigEditingField,
6
+ parseConfigEditingFieldInput,
7
+ patchForConfigEditingField,
8
+ readConfigLayers,
9
+ type ConfigEditingField,
10
+ type FieldId,
11
+ } from "../config/config-editing-model.js";
12
+ import {
13
+ createMemoryOperations,
14
+ type MemoryOperationsDeps,
15
+ } from "../operations/memory-operation-service.js";
16
+ import { type ConfigScope, type ProjectConfigPatchInput } from "../config/config-writer.js";
17
+ import { inputWithPrefill } from "./prefill-input.js";
18
+ import { CANCEL } from "./setup-tui-types.js";
19
+
20
+ const LOCAL_EMBED_GUIDANCE = [
21
+ "Local hindsight-embed guidance:",
22
+ "uvx hindsight-embed@latest profile create pi --port 8888",
23
+ "uvx hindsight-embed@latest -p pi bank create <bank-id>",
24
+ "uvx hindsight-embed@latest -p pi ui start",
25
+ ].join("\n");
26
+
27
+ async function writeAndReload(
28
+ ctx: ExtensionCommandContext,
29
+ deps: MemoryOperationsDeps,
30
+ patch: ProjectConfigPatchInput,
31
+ scope: ConfigScope = "project",
32
+ ): Promise<void> {
33
+ const result = await createMemoryOperations(deps).configure(ctx.cwd, { ...patch, scope });
34
+ ctx.ui.notify(`Saved ${scope} config to ${result.path}; setup view reloaded.`, "info");
35
+ }
36
+
37
+ async function resetField(
38
+ ctx: ExtensionCommandContext,
39
+ deps: MemoryOperationsDeps,
40
+ field: ConfigEditingField,
41
+ scope: ConfigScope = "project",
42
+ ): Promise<void> {
43
+ await writeAndReload(ctx, deps, { resetDefaults: [field.resetKey] }, scope);
44
+ }
45
+
46
+ function settingPrompt(field: ConfigEditingField): string {
47
+ return [
48
+ field.label,
49
+ field.description,
50
+ `Effective: ${field.value} (${field.source ?? "default"})`,
51
+ `Environment: ${field.envValue ?? "not set"}`,
52
+ `Project: ${field.projectValue ?? "not set"}`,
53
+ `User: ${field.globalValue ?? "not set"}`,
54
+ `Default: ${field.defaultValue}`,
55
+ field.source === "env"
56
+ ? "Environment currently wins; project/user edits save for when env override is removed."
57
+ : "Changes save immediately.",
58
+ ].join("\n");
59
+ }
60
+
61
+ function scopeLabel(scope: ConfigScope): string {
62
+ return scope === "project" ? "Project" : "User";
63
+ }
64
+
65
+ function withScopedValues(field: ConfigEditingField, values: string[]): string[] {
66
+ const scoped = (field.editableScopes ?? ["project"]).flatMap((scope) =>
67
+ values.map((value) => `${scopeLabel(scope)}: ${value}`),
68
+ );
69
+ const resets = (field.editableScopes ?? ["project"]).flatMap((scope) => {
70
+ if (scope === "project" && field.projectValue !== undefined) return ["Remove project override"];
71
+ if (scope === "global" && field.globalValue !== undefined) return ["Remove user override"];
72
+ return [];
73
+ });
74
+ return [...scoped, ...resets, CANCEL];
75
+ }
76
+
77
+ function parseScopedAction(action: string): { scope: ConfigScope; value: string } | undefined {
78
+ if (action.startsWith("Project: "))
79
+ return { scope: "project", value: action.slice("Project: ".length) };
80
+ if (action.startsWith("User: ")) return { scope: "global", value: action.slice("User: ".length) };
81
+ return undefined;
82
+ }
83
+
84
+ async function chooseScope(
85
+ ctx: ExtensionCommandContext,
86
+ field: ConfigEditingField,
87
+ ): Promise<ConfigScope | undefined> {
88
+ const scopes = field.editableScopes ?? ["project"];
89
+ if (scopes.length === 1) return scopes[0];
90
+ const value = await ctx.ui.select(settingPrompt(field), scopes.map(scopeLabel).concat(CANCEL));
91
+ if (!value || value === CANCEL) return undefined;
92
+ return value === "User" ? "global" : "project";
93
+ }
94
+
95
+ async function handleResetAction(
96
+ action: string,
97
+ ctx: ExtensionCommandContext,
98
+ deps: MemoryOperationsDeps,
99
+ field: ConfigEditingField,
100
+ ): Promise<boolean> {
101
+ if (action === "Remove project override") {
102
+ await resetField(ctx, deps, field, "project");
103
+ return true;
104
+ }
105
+ if (action === "Remove user override") {
106
+ await resetField(ctx, deps, field, "global");
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ async function promptScopedValue(
113
+ ctx: ExtensionCommandContext,
114
+ deps: MemoryOperationsDeps,
115
+ field: ConfigEditingField,
116
+ ): Promise<{ scope: ConfigScope; value: string } | undefined> {
117
+ if (field.kind === "boolean" || field.kind === "select") {
118
+ const values = field.kind === "boolean" ? ["Enable", "Disable"] : (field.choices ?? []);
119
+ const action = await ctx.ui.select(settingPrompt(field), withScopedValues(field, values));
120
+ if (!action || action === CANCEL) return undefined;
121
+ if (await handleResetAction(action, ctx, deps, field)) return undefined;
122
+ return parseScopedAction(action);
123
+ }
124
+
125
+ const scope = await chooseScope(ctx, field);
126
+ if (!scope) return undefined;
127
+ const value = await inputWithPrefill(
128
+ ctx.ui,
129
+ settingPrompt(field),
130
+ inputDefaultForConfigEditingField(field.id, deps.getConfig(), deps.getProjectBankId()),
131
+ );
132
+ const parsed = parseConfigEditingFieldInput(field, value);
133
+ if (parsed === undefined) return undefined;
134
+ return { scope, value: parsed };
135
+ }
136
+
137
+ export async function handleResetFieldAction(args: {
138
+ ctx: ExtensionCommandContext;
139
+ deps: MemoryOperationsDeps;
140
+ config: ResolvedConfig;
141
+ projectBankId: string;
142
+ fieldId: FieldId;
143
+ }): Promise<void> {
144
+ const field = buildConfigEditingFields(
145
+ args.config,
146
+ args.projectBankId,
147
+ readConfigLayers(args.ctx.cwd),
148
+ ).find((item) => item.id === args.fieldId);
149
+ if (field) {
150
+ await resetField(args.ctx, args.deps, field, field.source === "global" ? "global" : "project");
151
+ }
152
+ }
153
+
154
+ export async function handleFieldEdit(args: {
155
+ fieldId: FieldId;
156
+ ctx: ExtensionCommandContext;
157
+ deps: MemoryOperationsDeps;
158
+ config: ResolvedConfig;
159
+ projectBankId: string;
160
+ }): Promise<void> {
161
+ const layers = readConfigLayers(args.ctx.cwd);
162
+ const field = buildConfigEditingFields(args.config, args.projectBankId, layers).find(
163
+ (item) => item.id === args.fieldId,
164
+ );
165
+ if (!field) return;
166
+
167
+ const scoped = await promptScopedValue(args.ctx, args.deps, field);
168
+ if (!scoped) return;
169
+ const patch = patchForConfigEditingField(field.id, scoped.value, args.config);
170
+ if (patch) await writeAndReload(args.ctx, args.deps, patch, scoped.scope);
171
+ }
172
+
173
+ export async function handleDeployment(
174
+ ctx: ExtensionCommandContext,
175
+ deps: MemoryOperationsDeps,
176
+ config: ResolvedConfig,
177
+ ): Promise<void> {
178
+ const value = await ctx.ui.select("Hindsight deployment", [
179
+ "Hindsight Cloud",
180
+ "Existing local/external API",
181
+ "Local hindsight-embed guidance",
182
+ CANCEL,
183
+ ]);
184
+ if (value === "Hindsight Cloud") {
185
+ const baseUrl = await ctx.ui.input("Hindsight Cloud base URL", config.hindsight.baseUrl);
186
+ if (baseUrl) await writeAndReload(ctx, deps, { baseUrl: baseUrl.trim() });
187
+ const envName = await ctx.ui.input("API key env var name", "HINDSIGHT_API_TOKEN");
188
+ if (envName) await writeAndReload(ctx, deps, { apiKeyEnvVar: envName.trim() });
189
+ } else if (value === "Existing local/external API") {
190
+ const baseUrl = await ctx.ui.input("Hindsight API base URL", config.hindsight.baseUrl);
191
+ if (baseUrl) await writeAndReload(ctx, deps, { baseUrl: baseUrl.trim() });
192
+ } else if (value === "Local hindsight-embed guidance") {
193
+ ctx.ui.notify(LOCAL_EMBED_GUIDANCE, "info");
194
+ const useDefault = await ctx.ui.select("Set API URL to http://localhost:8888?", [
195
+ "Yes",
196
+ "No",
197
+ CANCEL,
198
+ ]);
199
+ if (useDefault === "Yes") await writeAndReload(ctx, deps, { baseUrl: "http://localhost:8888" });
200
+ }
201
+ }
@@ -0,0 +1,47 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ResolvedConfig } from "../types.js";
3
+ import {
4
+ buildConfigEditingTabs,
5
+ readConfigLayers,
6
+ type ConfigEditingTab,
7
+ } from "../config/config-editing-model.js";
8
+ import type { MemoryOperationsDeps } from "../operations/memory-operation-service.js";
9
+ import { listRetainReceipts } from "../lifecycle/retain-receipts.js";
10
+ import { collectStatusHealthFacts } from "../utils/status-health.js";
11
+ import { buildRetainReceiptStatusFacts } from "./setup-tui-render.js";
12
+ import { RECEIPT_FACT_LIMIT, type SetupUiState } from "./setup-tui-types.js";
13
+
14
+ export async function buildSetupTabs(args: {
15
+ ctx: ExtensionCommandContext;
16
+ config: ResolvedConfig;
17
+ projectBankId: string;
18
+ deps: MemoryOperationsDeps;
19
+ state: SetupUiState;
20
+ }): Promise<ConfigEditingTab[]> {
21
+ const statusFacts = await collectStatusHealthFacts({
22
+ client: args.deps.getClient(),
23
+ config: args.config,
24
+ projectBankId: args.projectBankId,
25
+ });
26
+ const receiptFacts = buildRetainReceiptStatusFacts(
27
+ await listRetainReceipts(args.ctx.cwd, RECEIPT_FACT_LIMIT),
28
+ );
29
+ return buildConfigEditingTabs(
30
+ args.config,
31
+ args.projectBankId,
32
+ readConfigLayers(args.ctx.cwd),
33
+ [...buildInitHealthFacts(args.deps.getInitHealth?.()), ...statusFacts, ...receiptFacts],
34
+ { showAdvanced: Boolean(args.state.showAdvanced) },
35
+ );
36
+ }
37
+
38
+ export function buildInitHealthFacts(
39
+ initHealth: ReturnType<NonNullable<MemoryOperationsDeps["getInitHealth"]>>,
40
+ ): Array<[string, string]> {
41
+ if (!initHealth) return [];
42
+ if (initHealth.failures.length === 0) return [["Startup", `ok (${initHealth.checkedAt})`]];
43
+ return initHealth.failures.map((failure) => [
44
+ `Startup ${failure.subsystem}`,
45
+ `failed: ${failure.error}`,
46
+ ]);
47
+ }
@@ -0,0 +1,239 @@
1
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
3
+ import type { ConfigEditingTab } from "../config/config-editing-model.js";
4
+ import type { RetainReceipt } from "../lifecycle/retain-receipts.js";
5
+ import { formatRetainOutcome } from "../queue/flush-presenter.js";
6
+ import {
7
+ applySetupIntent,
8
+ currentSetupTab,
9
+ normalizeSetupUiState,
10
+ selectedSetupField,
11
+ selectedSetupIndex,
12
+ setupIntentFromInput,
13
+ } from "./setup-flow.js";
14
+ import {
15
+ HUB_ACTION_HELP,
16
+ RECEIPT_FACT_LIMIT,
17
+ type SetupActionId,
18
+ type SetupUiState,
19
+ type ThemeLike,
20
+ } from "./setup-tui-types.js";
21
+
22
+ const MIN_BODY_LINES = 30;
23
+ const DOCS_BASE_URL = "https://luxus.github.io/pi-hindsight";
24
+
25
+ const TAB_DOCS: Record<string, string> = {
26
+ Status: `${DOCS_BASE_URL}/guides/use-hindsight-status/`,
27
+ Connection: `${DOCS_BASE_URL}/start/installation/`,
28
+ Banks: `${DOCS_BASE_URL}/concepts/memory-banks/`,
29
+ Recall: `${DOCS_BASE_URL}/concepts/retain-recall-reflect/`,
30
+ Retain: `${DOCS_BASE_URL}/concepts/queue-durability/`,
31
+ Import: `${DOCS_BASE_URL}/guides/importing-sessions/`,
32
+ UI: `${DOCS_BASE_URL}/start/setup-tui/`,
33
+ };
34
+
35
+ function fitColumns(left: string, right: string, width: number): string {
36
+ if (width <= 0) return "";
37
+ if (width < 40) return truncateToWidth(`${left} ${right}`, width);
38
+ const rightWidth = Math.min(Math.max(12, visibleWidth(right)), Math.floor(width * 0.38));
39
+ const leftWidth = Math.max(1, width - rightWidth - 1);
40
+ return `${truncateToWidth(left, leftWidth)} ${truncateToWidth(right, rightWidth)}`;
41
+ }
42
+
43
+ function borderLine(
44
+ width: number,
45
+ left: string,
46
+ fill: string,
47
+ right: string,
48
+ theme: ThemeLike,
49
+ ): string {
50
+ return theme.fg("borderAccent", `${left}${fill.repeat(Math.max(0, width - 2))}${right}`);
51
+ }
52
+
53
+ function padVisibleRight(content: string, width: number): string {
54
+ const truncated = truncateToWidth(content, width);
55
+ return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
56
+ }
57
+
58
+ function boxed(content: string, width: number, theme: ThemeLike): string {
59
+ if (width < 2) return truncateToWidth(content, width);
60
+ return `${theme.fg("borderAccent", "│")}${padVisibleRight(content, width - 2)}${theme.fg("borderAccent", "│")}`;
61
+ }
62
+
63
+ function shortDocumentId(documentId: string): string {
64
+ const parts = documentId.split(":");
65
+ if (parts[0] === "pi-explicit" && parts.length >= 3) return `${parts[0]}:${parts[2]}`;
66
+ return documentId;
67
+ }
68
+
69
+ function wrapWords(text: string, width: number): string[] {
70
+ if (width <= 0) return [""];
71
+ const words = text.split(/\s+/).filter(Boolean);
72
+ const lines: string[] = [];
73
+ let current = "";
74
+ for (const word of words) {
75
+ if (!current) current = word;
76
+ else if (`${current} ${word}`.length <= width) current = `${current} ${word}`;
77
+ else {
78
+ lines.push(current);
79
+ current = word;
80
+ }
81
+ }
82
+ if (current) lines.push(current);
83
+ return lines.length ? lines : [""];
84
+ }
85
+
86
+ export function buildRetainReceiptStatusFacts(receipts: RetainReceipt[]): Array<[string, string]> {
87
+ if (receipts.length === 0) return [["Retain receipts", "none"]];
88
+ return receipts.slice(0, RECEIPT_FACT_LIMIT).map((receipt, index) => {
89
+ const outcome = formatRetainOutcome(receipt.outcome);
90
+ const value = `${receipt.bankId} ${shortDocumentId(receipt.documentId)}${
91
+ outcome ? ` (${outcome})` : ""
92
+ }`;
93
+ return [index === 0 ? "Recent retain" : `Recent retain ${index + 1}`, value];
94
+ });
95
+ }
96
+
97
+ export function createSetupComponent(
98
+ tabs: ConfigEditingTab[],
99
+ theme: ThemeLike,
100
+ state: SetupUiState,
101
+ done: (action: SetupActionId | null) => void,
102
+ ): Component {
103
+ Object.assign(state, normalizeSetupUiState(tabs, state));
104
+
105
+ return {
106
+ render(width: number): string[] {
107
+ Object.assign(state, normalizeSetupUiState(tabs, state));
108
+ const tab = currentSetupTab(tabs, state) ?? tabs[0]!;
109
+ const lines: string[] = [];
110
+ const innerWidth = Math.max(0, width - 2);
111
+ const changedCount = tabs.reduce(
112
+ (count, item) => count + item.fields.filter((field) => field.changed).length,
113
+ 0,
114
+ );
115
+ const horizontal = new DynamicBorder((s: string) => theme.fg("borderAccent", s));
116
+
117
+ lines.push(...horizontal.render(width));
118
+ lines.push(borderLine(width, "╭", "─", "╮", theme));
119
+ lines.push(
120
+ boxed(
121
+ ` ${theme.fg("accent", theme.bold("Hindsight setup"))} ${theme.fg("dim", "saved immediately after edit · changed values marked *")}`,
122
+ width,
123
+ theme,
124
+ ),
125
+ );
126
+ lines.push(
127
+ boxed(` ${theme.fg("dim", `${changedCount} changed from defaults`)}`, width, theme),
128
+ );
129
+ lines.push(boxed("", width, theme));
130
+
131
+ const tabLine = tabs
132
+ .map((item, index) => {
133
+ const dirty = item.fields.some((field) => field.changed) ? "*" : "";
134
+ const label = ` ${item.id}${dirty} `;
135
+ return index === state.tabIndex
136
+ ? theme.bg("selectedBg", theme.fg("accent", theme.bold(label)))
137
+ : theme.fg("muted", label);
138
+ })
139
+ .join(" ");
140
+ lines.push(boxed(tabLine, width, theme));
141
+ lines.push(boxed("", width, theme));
142
+
143
+ lines.push(
144
+ boxed(
145
+ theme.fg(
146
+ "accent",
147
+ theme.bold(tab.id === "Status" ? "Memory status" : `${tab.id} settings`),
148
+ ),
149
+ width,
150
+ theme,
151
+ ),
152
+ );
153
+ if (tab.facts) {
154
+ for (const [label, value] of tab.facts) {
155
+ lines.push(
156
+ boxed(
157
+ fitColumns(
158
+ ` ${theme.fg("muted", `${label}:`)}`,
159
+ theme.fg("text", value),
160
+ innerWidth,
161
+ ),
162
+ width,
163
+ theme,
164
+ ),
165
+ );
166
+ }
167
+ } else {
168
+ lines.push(
169
+ boxed(theme.fg("dim", fitColumns("Setting", "Value", innerWidth)), width, theme),
170
+ );
171
+ }
172
+
173
+ for (const [index, field] of tab.fields.entries()) {
174
+ const isSelected = index === selectedSetupIndex(tabs, state);
175
+ const marker = isSelected ? theme.fg("accent", "→") : " ";
176
+ const dirty = field.changed ? theme.fg("warning", "*") : " ";
177
+ const label = isSelected
178
+ ? theme.fg("accent", theme.bold(field.label))
179
+ : theme.fg("text", field.label);
180
+ const rawValue = `${field.value} [${field.source ?? "default"}]`;
181
+ const value = field.changed ? theme.fg("warning", rawValue) : theme.fg("text", rawValue);
182
+ lines.push(
183
+ boxed(fitColumns(`${marker}${dirty} ${label}`, value, innerWidth), width, theme),
184
+ );
185
+ }
186
+
187
+ const detailField = selectedSetupField(tabs, state);
188
+ if (detailField) {
189
+ lines.push(boxed("", width, theme));
190
+ lines.push(boxed(theme.fg("accent", theme.bold("Details")), width, theme));
191
+ const details = [
192
+ `Docs: ${TAB_DOCS[detailField.tab] ?? `${DOCS_BASE_URL}/reference/configuration/`}`,
193
+ detailField.description,
194
+ `Default: ${detailField.defaultValue}`,
195
+ ...(detailField.projectValue ? [`Project: ${detailField.projectValue}`] : []),
196
+ ...(detailField.globalValue ? [`User: ${detailField.globalValue}`] : []),
197
+ ...(detailField.envValue ? [`Env: ${detailField.envValue}`] : []),
198
+ ];
199
+ for (const detail of details) {
200
+ for (const line of wrapWords(detail, innerWidth - 3)) {
201
+ lines.push(boxed(` ${theme.fg("dim", line)}`, width, theme));
202
+ }
203
+ }
204
+ } else if (tab.id === "Status") {
205
+ lines.push(boxed("", width, theme));
206
+ lines.push(boxed(theme.fg("accent", theme.bold("Docs")), width, theme));
207
+ for (const line of wrapWords(`Status guide: ${TAB_DOCS.Status}`, innerWidth - 3)) {
208
+ lines.push(boxed(` ${theme.fg("dim", line)}`, width, theme));
209
+ }
210
+ }
211
+
212
+ while (lines.length < MIN_BODY_LINES) lines.push(boxed("", width, theme));
213
+
214
+ lines.push(
215
+ boxed(
216
+ theme.fg(
217
+ "dim",
218
+ state.showAdvanced
219
+ ? ` advanced · h/l tabs · j/k · enter edit · r reset ·${HUB_ACTION_HELP}`
220
+ : ` hub ·${HUB_ACTION_HELP}`,
221
+ ),
222
+ width,
223
+ theme,
224
+ ),
225
+ );
226
+ lines.push(borderLine(width, "╰", "─", "╯", theme));
227
+ lines.push(...horizontal.render(width));
228
+ return lines.map((line) => truncateToWidth(line, width));
229
+ },
230
+ handleInput(data: string): void {
231
+ const intent = setupIntentFromInput(data);
232
+ if (!intent) return;
233
+ const result = applySetupIntent(tabs, state, intent);
234
+ Object.assign(state, result.state);
235
+ if (result.kind === "action") done(result.action);
236
+ },
237
+ invalidate(): void {},
238
+ };
239
+ }
@@ -0,0 +1,50 @@
1
+ import type { FieldId, TabId } from "../config/config-editing-model.js";
2
+
3
+ /** Day-to-day hub actions reachable from the `/hindsight` TUI. */
4
+ export type HubActionId =
5
+ | "choose-deployment"
6
+ | "guided-setup"
7
+ | "flush-queue"
8
+ | "set-mode"
9
+ | "next-opt-out"
10
+ | "apply-mental-models"
11
+ | "import-sessions"
12
+ | "run-doctor"
13
+ | "init-config"
14
+ | "toggle-advanced"
15
+ | "done";
16
+
17
+ export type SetupActionId = FieldId | `reset:${FieldId}` | HubActionId;
18
+
19
+ export type SetupStep = "config" | "profile" | "banks" | "review" | "done";
20
+
21
+ export type SetupProfileChoice =
22
+ | "project-user"
23
+ | "project-only"
24
+ | "isolated-only"
25
+ | "user-only"
26
+ | "recall-only";
27
+
28
+ export type SetupUiState = {
29
+ step?: SetupStep;
30
+ tabIndex: number;
31
+ selectedByTab: Partial<Record<TabId, number>>;
32
+ showAdvanced?: boolean;
33
+ profileChoice?: SetupProfileChoice;
34
+ };
35
+
36
+ export type ThemeLike = {
37
+ fg(
38
+ color: "accent" | "muted" | "dim" | "success" | "error" | "warning" | "borderAccent" | "text",
39
+ text: string,
40
+ ): string;
41
+ bg(color: "selectedBg", text: string): string;
42
+ bold(text: string): string;
43
+ };
44
+
45
+ export const RECEIPT_FACT_LIMIT = 3;
46
+ export const CANCEL = "Cancel";
47
+
48
+ /** Hub footer: status/setup/emergency first; import/templates/advanced demoted (agent tools own day-to-day edits). */
49
+ export const HUB_ACTION_HELP =
50
+ " g guided · m mode · x next-opt-out · f flush · o doctor · n init · d deployment · i import · t templates · a advanced · q close ";