@cortexkit/opencode-magic-context 0.23.1 → 0.24.1
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/README.md +6 -0
- package/dist/config/schema/magic-context.d.ts +10 -3
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +80 -0
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-embedding.d.ts +22 -26
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-backfill.d.ts +3 -2
- package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-cache.d.ts +3 -2
- package/dist/features/magic-context/memory/embedding-cache.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +2 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +17 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-provider.d.ts +2 -0
- package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +5 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -7
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +18 -12
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +53 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +14 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts +17 -0
- package/dist/features/magic-context/session-project-storage.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts +2 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +16 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +3 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +10 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +3 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +1 -0
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/workspaces.d.ts +20 -0
- package/dist/features/magic-context/workspaces.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts +23 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +5 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +11 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +7 -2
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +23 -3
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +33 -28
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +34 -17
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-structural-noise.d.ts +5 -7
- package/dist/hooks/magic-context/strip-structural-noise.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +4 -5
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.js +2411 -365
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +1 -1
- package/dist/shared/rpc-types.d.ts +1 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/tui-preferences.d.ts +32 -0
- package/dist/shared/tui-preferences.d.ts.map +1 -0
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shared/announcement.ts +6 -6
- package/src/shared/rpc-types.ts +1 -1
- package/src/shared/tui-preferences.test.ts +210 -0
- package/src/shared/tui-preferences.ts +303 -0
- package/src/tui/index.tsx +5 -3
- package/src/tui/slots/sidebar-content.tsx +123 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAO7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAuDizK,CAAC;;;;;;;;;;;;qBAA93D,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;
|
|
1
|
+
{"version":3,"file":"create-session-hooks.d.ts","sourceRoot":"","sources":["../../../src/plugin/hooks/create-session-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAO7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AACrF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;;;;;;qBAuDizK,CAAC;;;;;;;;;;;;qBAA93D,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAA+05B,CAAC;;;;;;EADh3gC"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Bump only when there are user-visible changes worth a startup dialog.
|
|
19
19
|
* Does NOT need to match the published package version.
|
|
20
20
|
*/
|
|
21
|
-
export declare const ANNOUNCEMENT_VERSION = "0.
|
|
21
|
+
export declare const ANNOUNCEMENT_VERSION = "0.24.0";
|
|
22
22
|
/**
|
|
23
23
|
* Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
|
|
24
24
|
* TUI dialog renders cleanly without horizontal scroll on a typical terminal.
|
|
@@ -77,7 +77,7 @@ export interface SidebarSnapshot {
|
|
|
77
77
|
*/
|
|
78
78
|
recompProgress?: {
|
|
79
79
|
/** "recomp" → "Recomp" labels; "upgrade" → "Upgrade" labels. */
|
|
80
|
-
kind?: "recomp" | "upgrade";
|
|
80
|
+
kind?: "recomp" | "upgrade" | "embed";
|
|
81
81
|
phase: "recomp" | "migration" | "done" | "failed" | "skipped";
|
|
82
82
|
processedMessages: number;
|
|
83
83
|
totalMessages: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-types.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;;OAQG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;;OAKG;IACH,cAAc,CAAC,EAAE;QACb,gEAAgE;QAChE,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc-types.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;;OAQG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;;OAKG;IACH,cAAc,CAAC,EAAE;QACb,gEAAgE;QAChE,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;QACtC,KAAK,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;QAC9D,iBAAiB,EAAE,MAAM,CAAC;QAC1B,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,YAAa,SAAQ,eAAe;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,oBAAoB,EAAE,YAAY,GAAG,QAAQ,CAAC;IAC9C;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uBAAuB,EAAE,MAAM,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const TUI_PREFS_FILE_ENV = "OPENCODE_TUI_PREFERENCES_FILE";
|
|
2
|
+
export declare function getTuiPreferencesFile(): string;
|
|
3
|
+
export declare function readTuiPreferencesFile(): Promise<Record<string, unknown>>;
|
|
4
|
+
export declare function readTuiPreferencesFileSync(): Record<string, unknown>;
|
|
5
|
+
export declare const PLUGIN_KEY = "magic-context";
|
|
6
|
+
export declare const DEFAULT_SLOT_ORDER = 200;
|
|
7
|
+
export interface MagicContextTuiPrefs {
|
|
8
|
+
forceToTop: boolean;
|
|
9
|
+
order: number;
|
|
10
|
+
startCollapsed: boolean;
|
|
11
|
+
rememberCollapsed: boolean;
|
|
12
|
+
collapsed: boolean | null;
|
|
13
|
+
header: {
|
|
14
|
+
label: string;
|
|
15
|
+
};
|
|
16
|
+
sections: {
|
|
17
|
+
historian: boolean;
|
|
18
|
+
memory: boolean;
|
|
19
|
+
status: boolean;
|
|
20
|
+
dreamer: boolean;
|
|
21
|
+
stats: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export type TuiSections = MagicContextTuiPrefs["sections"];
|
|
25
|
+
export declare const DEFAULT_PREFS: MagicContextTuiPrefs;
|
|
26
|
+
export declare function resolveMagicContextPrefs(root: Record<string, unknown>): MagicContextTuiPrefs;
|
|
27
|
+
export declare function computeEffectiveOrder(root: Record<string, unknown>, pluginKey: string, defaultOrder: number): number;
|
|
28
|
+
type JsonValue = string | number | boolean | null;
|
|
29
|
+
export declare function queueTuiPreferenceUpdate(pluginKey: string, path: string[], value: JsonValue): Promise<void>;
|
|
30
|
+
export declare function watchTuiPreferences(onChange: () => void): () => void;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=tui-preferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui-preferences.d.ts","sourceRoot":"","sources":["../../src/shared/tui-preferences.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,kBAAkB,kCAAkC,CAAC;AAGlE,wBAAgB,qBAAqB,IAAI,MAAM,CAO9C;AAQD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAS/E;AAMD,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASpE;AAED,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAC1C,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,OAAO,CAAC;IAE3B,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,QAAQ,EAAE;QACN,SAAS,EAAE,OAAO,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;KAClB,CAAC;CACL;AAED,MAAM,MAAM,WAAW,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;AAE3D,eAAO,MAAM,aAAa,EAAE,oBAc3B,CAAC;AAmBF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,oBAAoB,CAyB5F;AAgBD,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACrB,MAAM,CAOR;AASD,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AA0DlD,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,SAAS,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAeD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAuCpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/tools/ctx-memory/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/tools/ctx-memory/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAsChE,OAAO,EAKH,KAAK,iBAAiB,EACzB,MAAM,SAAS,CAAC;AA0oBjB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAI5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/tools/ctx-search/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAahE,OAAO,KAAK,EAAkC,iBAAiB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/tools/ctx-search/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAahE,OAAO,KAAK,EAAkC,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAoLjF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAI5F"}
|
package/package.json
CHANGED
|
@@ -23,18 +23,18 @@ import { getMagicContextStorageDir } from "./data-path";
|
|
|
23
23
|
* Bump only when there are user-visible changes worth a startup dialog.
|
|
24
24
|
* Does NOT need to match the published package version.
|
|
25
25
|
*/
|
|
26
|
-
export const ANNOUNCEMENT_VERSION = "0.
|
|
26
|
+
export const ANNOUNCEMENT_VERSION = "0.24.0";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
|
|
30
30
|
* TUI dialog renders cleanly without horizontal scroll on a typical terminal.
|
|
31
31
|
*/
|
|
32
32
|
export const ANNOUNCEMENT_FEATURES: ReadonlyArray<string> = [
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
33
|
+
"Searchable session history: ctx_search can now find older discussion by meaning, not just keywords. New history is embedded automatically — to backfill an EXISTING session's older history, run /ctx-embed-history once (it works in the background).",
|
|
34
|
+
"Cross-project workspaces: group related repos and share project memories across them, with per-category control over what's shared. Set them up in the dashboard's Workspaces panel.",
|
|
35
|
+
"Pi: fixed sessions overflowing the model context while still showing moderate usage — Pi now sheds context before a tool-heavy turn overflows.",
|
|
36
|
+
"Fewer prompt-cache busts: doc edits, processed screenshots, and a rebuild-then-bust-again case no longer re-bill large prompt prefixes.",
|
|
37
|
+
"Setup wizard now lists your actual models with type-ahead instead of fixed recommendations, and explains the historian/dreamer roles (issue #144). Plus a GitHub Copilot tool-pairing fix (#135).",
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
/**
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface SidebarSnapshot {
|
|
|
78
78
|
*/
|
|
79
79
|
recompProgress?: {
|
|
80
80
|
/** "recomp" → "Recomp" labels; "upgrade" → "Upgrade" labels. */
|
|
81
|
-
kind?: "recomp" | "upgrade";
|
|
81
|
+
kind?: "recomp" | "upgrade" | "embed";
|
|
82
82
|
phase: "recomp" | "migration" | "done" | "failed" | "skipped";
|
|
83
83
|
processedMessages: number;
|
|
84
84
|
totalMessages: number;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parse } from "comment-json";
|
|
6
|
+
import {
|
|
7
|
+
computeEffectiveOrder,
|
|
8
|
+
DEFAULT_PREFS,
|
|
9
|
+
DEFAULT_SLOT_ORDER,
|
|
10
|
+
getTuiPreferencesFile,
|
|
11
|
+
PLUGIN_KEY,
|
|
12
|
+
queueTuiPreferenceUpdate,
|
|
13
|
+
readTuiPreferencesFile,
|
|
14
|
+
resolveMagicContextPrefs,
|
|
15
|
+
TUI_PREFS_FILE_ENV,
|
|
16
|
+
} from "./tui-preferences";
|
|
17
|
+
|
|
18
|
+
let dir: string;
|
|
19
|
+
let file: string;
|
|
20
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
21
|
+
const ENV_KEYS = [TUI_PREFS_FILE_ENV, "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME"];
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
for (const key of ENV_KEYS) savedEnv[key] = process.env[key];
|
|
25
|
+
dir = await mkdtemp(join(tmpdir(), "mc-tui-prefs-test-"));
|
|
26
|
+
file = join(dir, "tui-preferences.jsonc");
|
|
27
|
+
process.env[TUI_PREFS_FILE_ENV] = file;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
for (const key of ENV_KEYS) {
|
|
32
|
+
if (savedEnv[key] === undefined) delete process.env[key];
|
|
33
|
+
else process.env[key] = savedEnv[key];
|
|
34
|
+
}
|
|
35
|
+
await rm(dir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("getTuiPreferencesFile", () => {
|
|
39
|
+
test("env override wins", () => {
|
|
40
|
+
expect(getTuiPreferencesFile()).toBe(file);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("falls back to OPENCODE_CONFIG_DIR then XDG then ~/.config", () => {
|
|
44
|
+
delete process.env[TUI_PREFS_FILE_ENV];
|
|
45
|
+
process.env.OPENCODE_CONFIG_DIR = "/tmp/cfgdir";
|
|
46
|
+
expect(getTuiPreferencesFile()).toBe("/tmp/cfgdir/tui-preferences.jsonc");
|
|
47
|
+
delete process.env.OPENCODE_CONFIG_DIR;
|
|
48
|
+
process.env.XDG_CONFIG_HOME = "/tmp/xdg";
|
|
49
|
+
expect(getTuiPreferencesFile()).toBe("/tmp/xdg/opencode/tui-preferences.jsonc");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("readTuiPreferencesFile (tolerant)", () => {
|
|
54
|
+
test("missing file → {}", async () => {
|
|
55
|
+
expect(await readTuiPreferencesFile()).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("malformed JSON → {}", async () => {
|
|
59
|
+
await writeFile(file, "{ this is not json ", "utf8");
|
|
60
|
+
expect(await readTuiPreferencesFile()).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("non-object root → {}", async () => {
|
|
64
|
+
await writeFile(file, "[1, 2, 3]", "utf8");
|
|
65
|
+
expect(await readTuiPreferencesFile()).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("jsonc with comments + trailing comma parses", async () => {
|
|
69
|
+
await writeFile(
|
|
70
|
+
file,
|
|
71
|
+
`{
|
|
72
|
+
// a comment
|
|
73
|
+
"magic-context": { "order": 205, },
|
|
74
|
+
}`,
|
|
75
|
+
"utf8",
|
|
76
|
+
);
|
|
77
|
+
const root = await readTuiPreferencesFile();
|
|
78
|
+
expect(resolveMagicContextPrefs(root).order).toBe(205);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("resolveMagicContextPrefs (per-key validation)", () => {
|
|
83
|
+
test("missing key → full defaults clone", () => {
|
|
84
|
+
expect(resolveMagicContextPrefs({})).toEqual(DEFAULT_PREFS);
|
|
85
|
+
// clone, not the shared object
|
|
86
|
+
expect(resolveMagicContextPrefs({})).not.toBe(DEFAULT_PREFS);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("one bad value never poisons the rest", () => {
|
|
90
|
+
const prefs = resolveMagicContextPrefs({
|
|
91
|
+
"magic-context": {
|
|
92
|
+
order: "nope",
|
|
93
|
+
rememberCollapsed: 1,
|
|
94
|
+
collapsed: true,
|
|
95
|
+
sections: { historian: false, memory: "bad" },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(prefs.order).toBe(DEFAULT_SLOT_ORDER); // bad → default
|
|
99
|
+
expect(prefs.rememberCollapsed).toBe(true); // bad → default true
|
|
100
|
+
expect(prefs.collapsed).toBe(true); // valid bool preserved
|
|
101
|
+
expect(prefs.sections.historian).toBe(false); // valid bool preserved
|
|
102
|
+
expect(prefs.sections.memory).toBe(true); // bad → default true
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("order clamps to -10000..10000", () => {
|
|
106
|
+
expect(resolveMagicContextPrefs({ "magic-context": { order: 99999 } }).order).toBe(10000);
|
|
107
|
+
expect(resolveMagicContextPrefs({ "magic-context": { order: -99999 } }).order).toBe(-10000);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("collapsed non-boolean → null (seed from startCollapsed)", () => {
|
|
111
|
+
expect(resolveMagicContextPrefs({ "magic-context": {} }).collapsed).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("header label clamps length, empty → default", () => {
|
|
115
|
+
expect(
|
|
116
|
+
resolveMagicContextPrefs({ "magic-context": { header: { label: "" } } }).header.label,
|
|
117
|
+
).toBe(DEFAULT_PREFS.header.label);
|
|
118
|
+
expect(
|
|
119
|
+
resolveMagicContextPrefs({
|
|
120
|
+
"magic-context": { header: { label: "x".repeat(50) } },
|
|
121
|
+
}).header.label.length,
|
|
122
|
+
).toBe(24);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("computeEffectiveOrder (cross-plugin convention)", () => {
|
|
127
|
+
test("default when key missing", () => {
|
|
128
|
+
expect(computeEffectiveOrder({}, PLUGIN_KEY, DEFAULT_SLOT_ORDER)).toBe(DEFAULT_SLOT_ORDER);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("explicit order clamped", () => {
|
|
132
|
+
expect(computeEffectiveOrder({ "magic-context": { order: 250 } }, PLUGIN_KEY, 200)).toBe(
|
|
133
|
+
250,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("forceToTop sorts below FORCE_TOP_BASE by key position", () => {
|
|
138
|
+
const root = { aft: { forceToTop: true }, "magic-context": { forceToTop: true } };
|
|
139
|
+
expect(computeEffectiveOrder(root, "aft", 200)).toBe(-100000 + 0);
|
|
140
|
+
expect(computeEffectiveOrder(root, "magic-context", 200)).toBe(-100000 + 1);
|
|
141
|
+
// forced always beats any manual order (clamped band is strictly above)
|
|
142
|
+
expect(computeEffectiveOrder(root, "aft", 200)).toBeLessThan(-10000);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("write path — comment-json full round-trip", () => {
|
|
147
|
+
test("persists a nested key and reads back", async () => {
|
|
148
|
+
await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
|
|
149
|
+
const prefs = resolveMagicContextPrefs(await readTuiPreferencesFile());
|
|
150
|
+
expect(prefs.collapsed).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("seeds the file from the template when absent", async () => {
|
|
154
|
+
await queueTuiPreferenceUpdate(PLUGIN_KEY, ["order"], 205);
|
|
155
|
+
const text = await readFile(file, "utf8");
|
|
156
|
+
expect(text).toContain("Shared preferences for OpenCode TUI plugins");
|
|
157
|
+
expect(resolveMagicContextPrefs(await readTuiPreferencesFile()).order).toBe(205);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("INTEROP: a sibling plugin's values AND comments survive MC writing only its key", async () => {
|
|
161
|
+
// A shared file owned partly by anthropic-auth, with comments and an
|
|
162
|
+
// appearance block MC knows nothing about. MC must touch ONLY its key.
|
|
163
|
+
await writeFile(
|
|
164
|
+
file,
|
|
165
|
+
`{
|
|
166
|
+
// anthropic-auth section — DO NOT lose this BLOCK comment
|
|
167
|
+
"anthropic-auth": {
|
|
168
|
+
"order": 160,
|
|
169
|
+
"header": { "label": "CLAUDE" },
|
|
170
|
+
// bar appearance knobs MC has no schema for
|
|
171
|
+
"appearance": { "barWidth": 10, "barFilledChar": "#" },
|
|
172
|
+
"pollMs": 2000 // INLINE trailing comment — must survive too
|
|
173
|
+
},
|
|
174
|
+
"magic-context": { "order": 200 }
|
|
175
|
+
}
|
|
176
|
+
`,
|
|
177
|
+
"utf8",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
|
|
181
|
+
|
|
182
|
+
const text = await readFile(file, "utf8");
|
|
183
|
+
// sibling comments preserved — BOTH block and inline trailing
|
|
184
|
+
// (comment-json round-trips both faithfully; enforce the guarantee).
|
|
185
|
+
expect(text).toContain("anthropic-auth section — DO NOT lose this BLOCK comment");
|
|
186
|
+
expect(text).toContain("bar appearance knobs MC has no schema for");
|
|
187
|
+
expect(text).toContain("INLINE trailing comment — must survive too");
|
|
188
|
+
|
|
189
|
+
// sibling VALUES intact (incl. nested keys MC has no schema for)
|
|
190
|
+
const root = parse(text) as Record<string, Record<string, unknown>>;
|
|
191
|
+
const aa = root["anthropic-auth"] as Record<string, unknown>;
|
|
192
|
+
expect(aa.order).toBe(160);
|
|
193
|
+
expect((aa.header as Record<string, unknown>).label).toBe("CLAUDE");
|
|
194
|
+
const appearance = aa.appearance as Record<string, unknown>;
|
|
195
|
+
expect(appearance.barWidth).toBe(10);
|
|
196
|
+
expect(appearance.barFilledChar).toBe("#");
|
|
197
|
+
|
|
198
|
+
// MC's own change landed
|
|
199
|
+
expect(resolveMagicContextPrefs(root).collapsed).toBe(true);
|
|
200
|
+
expect(resolveMagicContextPrefs(root).order).toBe(200);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("malformed existing file → write is a no-op, sibling content untouched", async () => {
|
|
204
|
+
const broken = `{ "anthropic-auth": { "order": 160 } broken `;
|
|
205
|
+
await writeFile(file, broken, "utf8");
|
|
206
|
+
await queueTuiPreferenceUpdate(PLUGIN_KEY, ["collapsed"], true);
|
|
207
|
+
// unchanged — we never clobber a file we can't safely parse
|
|
208
|
+
expect(await readFile(file, "utf8")).toBe(broken);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFileSync, watch } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { parse, stringify } from "comment-json";
|
|
6
|
+
|
|
7
|
+
// Shared preferences file for OpenCode TUI plugins. One top-level key per plugin
|
|
8
|
+
// (short, non-integer-like name, e.g. "magic-context"). The file is OPTIONAL:
|
|
9
|
+
// every reader falls back to defaults when it is missing or malformed.
|
|
10
|
+
//
|
|
11
|
+
// Cross-plugin convention (anthropic-auth / aft / magic-context all mirror it):
|
|
12
|
+
// - same file name + env override + lookup order,
|
|
13
|
+
// - byte-identical `computeEffectiveOrder` so the three sort consistently,
|
|
14
|
+
// - a coordinated default-order ladder (anthropic-auth 160, AFT 180, MC 200).
|
|
15
|
+
//
|
|
16
|
+
// MC uses `comment-json` (already a dep, Bun-safe) for the WRITE path — a full
|
|
17
|
+
// parse → mutate-one-key → stringify round-trip that preserves comments and
|
|
18
|
+
// sibling plugins' keys. (anthropic-auth uses jsonc-parser's surgical `modify`;
|
|
19
|
+
// AFT and MC use comment-json. Both are interop-safe as long as a sibling key's
|
|
20
|
+
// values AND comments survive — asserted by the interop test.)
|
|
21
|
+
|
|
22
|
+
export const TUI_PREFS_FILE_ENV = "OPENCODE_TUI_PREFERENCES_FILE";
|
|
23
|
+
const FILE_NAME = "tui-preferences.jsonc";
|
|
24
|
+
|
|
25
|
+
export function getTuiPreferencesFile(): string {
|
|
26
|
+
const override = process.env[TUI_PREFS_FILE_ENV];
|
|
27
|
+
if (override) return override;
|
|
28
|
+
const configDir =
|
|
29
|
+
process.env.OPENCODE_CONFIG_DIR ||
|
|
30
|
+
join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
31
|
+
return join(configDir, FILE_NAME);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Tolerant read: a missing file, parse error, or non-object root all resolve to
|
|
39
|
+
// {} so the sidebar never crashes on hand-edited content. Never throws.
|
|
40
|
+
export async function readTuiPreferencesFile(): Promise<Record<string, unknown>> {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(getTuiPreferencesFile(), "utf8");
|
|
43
|
+
if (raw.trim() === "") return {};
|
|
44
|
+
const root: unknown = parse(raw);
|
|
45
|
+
return isRecord(root) ? (root as Record<string, unknown>) : {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Synchronous tolerant read — used once at slot mount to seed the initial
|
|
52
|
+
// collapse state and effective order WITHOUT a frame of async flicker (the
|
|
53
|
+
// sidebar must render at its final width/collapse on the very first paint).
|
|
54
|
+
// Same tolerance contract as the async reader. Never throws.
|
|
55
|
+
export function readTuiPreferencesFileSync(): Record<string, unknown> {
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync(getTuiPreferencesFile(), "utf8");
|
|
58
|
+
if (raw.trim() === "") return {};
|
|
59
|
+
const root: unknown = parse(raw);
|
|
60
|
+
return isRecord(root) ? (root as Record<string, unknown>) : {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const PLUGIN_KEY = "magic-context";
|
|
67
|
+
export const DEFAULT_SLOT_ORDER = 200;
|
|
68
|
+
|
|
69
|
+
export interface MagicContextTuiPrefs {
|
|
70
|
+
forceToTop: boolean;
|
|
71
|
+
order: number;
|
|
72
|
+
startCollapsed: boolean;
|
|
73
|
+
rememberCollapsed: boolean;
|
|
74
|
+
// null = never persisted; seed the UI from `startCollapsed` instead.
|
|
75
|
+
collapsed: boolean | null;
|
|
76
|
+
header: {
|
|
77
|
+
label: string;
|
|
78
|
+
};
|
|
79
|
+
sections: {
|
|
80
|
+
historian: boolean;
|
|
81
|
+
memory: boolean;
|
|
82
|
+
status: boolean;
|
|
83
|
+
dreamer: boolean;
|
|
84
|
+
stats: boolean;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type TuiSections = MagicContextTuiPrefs["sections"];
|
|
89
|
+
|
|
90
|
+
export const DEFAULT_PREFS: MagicContextTuiPrefs = {
|
|
91
|
+
forceToTop: false,
|
|
92
|
+
order: DEFAULT_SLOT_ORDER,
|
|
93
|
+
startCollapsed: false,
|
|
94
|
+
rememberCollapsed: true,
|
|
95
|
+
collapsed: null,
|
|
96
|
+
header: { label: "Magic Context" },
|
|
97
|
+
sections: {
|
|
98
|
+
historian: true,
|
|
99
|
+
memory: true,
|
|
100
|
+
status: true,
|
|
101
|
+
dreamer: true,
|
|
102
|
+
stats: true,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function bool(value: unknown, fallback: boolean): boolean {
|
|
107
|
+
return typeof value === "boolean" ? value : fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function int(value: unknown, fallback: number, min: number, max: number): number {
|
|
111
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
112
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function label(value: unknown, fallback: string, maxLength: number): string {
|
|
116
|
+
if (typeof value !== "string" || value.length === 0) return fallback;
|
|
117
|
+
return value.slice(0, maxLength);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Per-key validation: every value is independently clamped/defaulted so one bad
|
|
121
|
+
// entry never poisons the rest. Never throws. A missing/non-object MC key →
|
|
122
|
+
// full defaults clone.
|
|
123
|
+
export function resolveMagicContextPrefs(root: Record<string, unknown>): MagicContextTuiPrefs {
|
|
124
|
+
const entry = root[PLUGIN_KEY];
|
|
125
|
+
if (!isRecord(entry)) return structuredClone(DEFAULT_PREFS);
|
|
126
|
+
|
|
127
|
+
const d = DEFAULT_PREFS;
|
|
128
|
+
const header = isRecord(entry.header) ? entry.header : {};
|
|
129
|
+
const sections = isRecord(entry.sections) ? entry.sections : {};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
forceToTop: bool(entry.forceToTop, d.forceToTop),
|
|
133
|
+
order: int(entry.order, d.order, -10000, 10000),
|
|
134
|
+
startCollapsed: bool(entry.startCollapsed, d.startCollapsed),
|
|
135
|
+
rememberCollapsed: bool(entry.rememberCollapsed, d.rememberCollapsed),
|
|
136
|
+
collapsed: typeof entry.collapsed === "boolean" ? entry.collapsed : null,
|
|
137
|
+
header: {
|
|
138
|
+
label: label(header.label, d.header.label, 24),
|
|
139
|
+
},
|
|
140
|
+
sections: {
|
|
141
|
+
historian: bool(sections.historian, d.sections.historian),
|
|
142
|
+
memory: bool(sections.memory, d.sections.memory),
|
|
143
|
+
status: bool(sections.status, d.sections.status),
|
|
144
|
+
dreamer: bool(sections.dreamer, d.sections.dreamer),
|
|
145
|
+
stats: bool(sections.stats, d.sections.stats),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const FORCE_TOP_BASE = -100000;
|
|
151
|
+
|
|
152
|
+
// Shared forceToTop convention — MUST stay byte-identical across anthropic-auth,
|
|
153
|
+
// AFT, and magic-context or the three sort inconsistently against each other.
|
|
154
|
+
// Forced plugins sort below FORCE_TOP_BASE, ordered among themselves by their
|
|
155
|
+
// top-level key's position in the file, so users reprioritize by reordering
|
|
156
|
+
// keys. The user-facing `order` knob clamps to -10000..10000, strictly above the
|
|
157
|
+
// forced band, so a manual order can never beat forceToTop. Host slots render
|
|
158
|
+
// ascending by order; OpenCode's built-ins occupy 100-500.
|
|
159
|
+
//
|
|
160
|
+
// Key-naming requirement: plugin keys must be non-integer-like short names (e.g.
|
|
161
|
+
// "magic-context"). JS object key iteration hoists integer-like keys ("0", "42")
|
|
162
|
+
// ahead of string keys, which would skew the indexOf-based ordering of forced
|
|
163
|
+
// plugins. The shared convention requires non-numeric names.
|
|
164
|
+
export function computeEffectiveOrder(
|
|
165
|
+
root: Record<string, unknown>,
|
|
166
|
+
pluginKey: string,
|
|
167
|
+
defaultOrder: number,
|
|
168
|
+
): number {
|
|
169
|
+
const entry = root[pluginKey];
|
|
170
|
+
if (!isRecord(entry)) return defaultOrder;
|
|
171
|
+
if (entry.forceToTop === true) {
|
|
172
|
+
return FORCE_TOP_BASE + Object.keys(root).indexOf(pluginKey);
|
|
173
|
+
}
|
|
174
|
+
return int(entry.order, defaultOrder, -10000, 10000);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const TEMPLATE = `// Shared preferences for OpenCode TUI plugins.
|
|
178
|
+
// One top-level key per plugin (short name). See each plugin's README for its
|
|
179
|
+
// supported settings. This file is safe to hand-edit; plugins update individual
|
|
180
|
+
// keys and preserve the rest (values and comments).
|
|
181
|
+
{}
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
type JsonValue = string | number | boolean | null;
|
|
185
|
+
|
|
186
|
+
// Set a nested path on a comment-json root, creating intermediate plain objects
|
|
187
|
+
// as needed. Mutating an existing leaf preserves its comments; sibling keys are
|
|
188
|
+
// untouched. Returns false when the path is blocked by a non-object value.
|
|
189
|
+
function setDeep(root: Record<string, unknown>, path: string[], value: JsonValue): boolean {
|
|
190
|
+
let node: Record<string, unknown> = root;
|
|
191
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
192
|
+
const key = path[i];
|
|
193
|
+
const child = node[key];
|
|
194
|
+
if (child === undefined || child === null) {
|
|
195
|
+
node[key] = {};
|
|
196
|
+
} else if (!isRecord(child)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
node = node[key] as Record<string, unknown>;
|
|
200
|
+
}
|
|
201
|
+
node[path[path.length - 1]] = value;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function writePreference(pluginKey: string, path: string[], value: JsonValue): Promise<void> {
|
|
206
|
+
const file = getTuiPreferencesFile();
|
|
207
|
+
await mkdir(dirname(file), { recursive: true });
|
|
208
|
+
let text: string;
|
|
209
|
+
try {
|
|
210
|
+
text = await readFile(file, "utf8");
|
|
211
|
+
} catch {
|
|
212
|
+
text = "";
|
|
213
|
+
}
|
|
214
|
+
if (text.trim() === "") text = TEMPLATE;
|
|
215
|
+
|
|
216
|
+
let root: unknown;
|
|
217
|
+
try {
|
|
218
|
+
root = parse(text);
|
|
219
|
+
} catch {
|
|
220
|
+
// The shared file is currently malformed. Skip the write rather than
|
|
221
|
+
// clobber sibling plugins' keys — the user fixes the file, persistence
|
|
222
|
+
// resumes. (Collapse just won't survive restart until then.)
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!isRecord(root)) root = {};
|
|
226
|
+
if (!setDeep(root as Record<string, unknown>, [pluginKey, ...path], value)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const next = `${stringify(root, null, 2)}\n`;
|
|
231
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
232
|
+
await writeFile(tmp, next, "utf8");
|
|
233
|
+
await rename(tmp, file);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
237
|
+
|
|
238
|
+
// Writes are serialized on a promise chain: each update re-reads the file,
|
|
239
|
+
// applies a comment-preserving edit to one property, and replaces the file
|
|
240
|
+
// atomically (temp + rename in the same directory — the only safe cross-process
|
|
241
|
+
// swap). Best-effort by design; preferences are never worth crashing the TUI.
|
|
242
|
+
export function queueTuiPreferenceUpdate(
|
|
243
|
+
pluginKey: string,
|
|
244
|
+
path: string[],
|
|
245
|
+
value: JsonValue,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
writeChain = writeChain.then(() => writePreference(pluginKey, path, value)).catch(() => {});
|
|
248
|
+
return writeChain;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const WATCH_DEBOUNCE_MS = 150;
|
|
252
|
+
|
|
253
|
+
// Watches the DIRECTORY, not the file: editors and our own atomic writes replace
|
|
254
|
+
// the file via rename, which kills file-level watchers.
|
|
255
|
+
//
|
|
256
|
+
// Two-stage filtering: (1) a cheap filename pre-filter on the prefs name or our
|
|
257
|
+
// `.tmp`; (2) inside the debounce, re-read and compare against last-seen content
|
|
258
|
+
// — the authority. Some platforms (macOS FSEvents, some inotify backends)
|
|
259
|
+
// misattribute a sibling rename to the real filename, so a name filter alone
|
|
260
|
+
// still produces strays; the content compare is robust against that, coalesced
|
|
261
|
+
// events, and mtime granularity.
|
|
262
|
+
//
|
|
263
|
+
// Returns a disposer; never throws.
|
|
264
|
+
export function watchTuiPreferences(onChange: () => void): () => void {
|
|
265
|
+
const file = getTuiPreferencesFile();
|
|
266
|
+
const name = basename(file);
|
|
267
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
268
|
+
let lastSeen: string | null = null;
|
|
269
|
+
// Seed asynchronously; a real change before the seed resolves still wins
|
|
270
|
+
// because the debounce re-reads fresh and compares against `lastSeen` (null
|
|
271
|
+
// → does not match → fires).
|
|
272
|
+
void readFile(file, "utf8")
|
|
273
|
+
.then((text) => {
|
|
274
|
+
if (lastSeen === null) lastSeen = text;
|
|
275
|
+
})
|
|
276
|
+
.catch(() => {});
|
|
277
|
+
try {
|
|
278
|
+
const watcher = watch(dirname(file), (_event, filename) => {
|
|
279
|
+
const isOurs =
|
|
280
|
+
filename === name ||
|
|
281
|
+
(filename?.startsWith(`${name}.`) && filename.endsWith(".tmp"));
|
|
282
|
+
if (filename != null && !isOurs) return;
|
|
283
|
+
if (timer) clearTimeout(timer);
|
|
284
|
+
timer = setTimeout(() => {
|
|
285
|
+
timer = null;
|
|
286
|
+
void readFile(file, "utf8")
|
|
287
|
+
.catch(() => null)
|
|
288
|
+
.then((text) => {
|
|
289
|
+
if (text === null) return;
|
|
290
|
+
if (text === lastSeen) return;
|
|
291
|
+
lastSeen = text;
|
|
292
|
+
onChange();
|
|
293
|
+
});
|
|
294
|
+
}, WATCH_DEBOUNCE_MS);
|
|
295
|
+
});
|
|
296
|
+
return () => {
|
|
297
|
+
if (timer) clearTimeout(timer);
|
|
298
|
+
watcher.close();
|
|
299
|
+
};
|
|
300
|
+
} catch {
|
|
301
|
+
return () => {};
|
|
302
|
+
}
|
|
303
|
+
}
|