@cortexkit/opencode-magic-context 0.21.6 → 0.21.7
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 +1 -1
- package/dist/config/agent-disable.d.ts +26 -0
- package/dist/config/agent-disable.d.ts.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +0 -6
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-lease.d.ts +14 -0
- package/dist/features/magic-context/compartment-lease.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-storage.d.ts +5 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/compression-depth-storage.d.ts +2 -1
- package/dist/features/magic-context/compression-depth-storage.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- 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.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner-compressor.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 +2 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +5 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -2
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +2 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +617 -173
- package/dist/plugin/conflict-warning-hook.d.ts +10 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +55 -0
- package/dist/shared/announcement.d.ts.map +1 -0
- package/dist/shared/format-threshold.d.ts +24 -0
- package/dist/shared/format-threshold.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts +14 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/shared/announcement.test.ts +143 -0
- package/src/shared/announcement.ts +97 -0
- package/src/shared/format-threshold.ts +28 -0
- package/src/tui/data/context-db.ts +43 -0
- package/src/tui/index.tsx +68 -4
- package/src/tui/slots/sidebar-content.tsx +11 -2
|
@@ -21,4 +21,14 @@ export declare function cleanupConflictWarnings(client: unknown, directory: stri
|
|
|
21
21
|
* Sends an ignored message that auto-deletes after 1 second.
|
|
22
22
|
*/
|
|
23
23
|
export declare function sendTuiSetupNotification(client: unknown, directory: string, serverUrl?: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Desktop startup announcement: post a one-shot ignored message describing
|
|
26
|
+
* what's new in this release. Mirrors the TUI's RPC-driven dialog path so both
|
|
27
|
+
* surfaces deliver the same announcement once per ANNOUNCEMENT_VERSION.
|
|
28
|
+
*
|
|
29
|
+
* Persistence lives in `getMagicContextStorageDir()/last_announced_version`,
|
|
30
|
+
* shared with the TUI handlers and the Pi plugin so a dismissal in any harness
|
|
31
|
+
* suppresses the others for the same announcement.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sendStartupAnnouncement(client: unknown, directory: string, version: string, features: ReadonlyArray<string>, footer: string, markSeen: (version: string) => void): Promise<void>;
|
|
24
34
|
//# sourceMappingURL=conflict-warning-hook.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conflict-warning-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/conflict-warning-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"conflict-warning-hook.d.ts","sourceRoot":"","sources":["../../src/plugin/conflict-warning-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAoLlE;;GAEG;AACH,wBAAsB,mBAAmB,CACrC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,cAAc,GAC/B,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAqHf;AAkCD;;;GAGG;AACH,wBAAsB,wBAAwB,CAC1C,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAkEf;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CACzC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACpC,OAAO,CAAC,IAAI,CAAC,CA8Df"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dream-timer.d.ts","sourceRoot":"","sources":["../../src/plugin/dream-timer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAapE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAK7C;;;;;GAKG;AACH,UAAU,mBAAmB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wBAAwB,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uBAAuB,CAAC,EAAE;QACtB,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,iBAAiB,CAAC,EAAE;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxE;AAQD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CACzC,IAAI,EAAE,mBAAmB,GAC1B,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"dream-timer.d.ts","sourceRoot":"","sources":["../../src/plugin/dream-timer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAapE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAK7C;;;;;GAKG;AACH,UAAU,mBAAmB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wBAAwB,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,uBAAuB,CAAC,EAAE;QACtB,OAAO,EAAE,OAAO,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,iBAAiB,CAAC,EAAE;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxE;AAQD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CACzC,IAAI,EAAE,mBAAmB,GAC1B,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CA4DnC"}
|
|
@@ -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;AAU7D,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;;;;;;qBAoDkuJ,CAAC;;;;;;;;;;;;qBAA9sC,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;AAU7D,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;;;;;;qBAoDkuJ,CAAC;;;;;;;;;;;;qBAA9sC,CAAC;mBAAyB,CAAC;iBAAuB,CAAC;iBAAuB,CAAC;0BAAc,CAAC;uBAAiB,CAAC;;;;;;0BAA+luB,CAAC;;;;;;EADju1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,eAAe,IAAI,QAAQ,EAAgB,MAAM,mCAAmC,CAAC;AAWnG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,eAAe,IAAI,QAAQ,EAAgB,MAAM,mCAAmC,CAAC;AAWnG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAgBlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAmDzE,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,EAK9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,eAAe,CA6UjB;AAED,wBAAgB,iBAAiB,CAC7B,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,qBAAqB,CAAC,EAAE,MAAM,GAC/B,YAAY,CAuKd;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAkKN"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../../src/plugin/tool-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../../src/plugin/tool-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAgB1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,GAAG,EAAE,aAAa,CAAC;IACnB,YAAY,EAAE,wBAAwB,CAAC;CAC1C,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAyEjC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-notes startup announcement shared by OpenCode plugin and Pi plugin.
|
|
3
|
+
*
|
|
4
|
+
* Bump `ANNOUNCEMENT_VERSION` and populate `ANNOUNCEMENT_FEATURES` *only* when a
|
|
5
|
+
* release ships user-facing news worth surfacing once at startup. Patch releases
|
|
6
|
+
* with no user-visible changes should leave both untouched — that way a user who
|
|
7
|
+
* already dismissed the dialog for the current `ANNOUNCEMENT_VERSION` won't see
|
|
8
|
+
* it again on the next bugfix bump.
|
|
9
|
+
*
|
|
10
|
+
* The persisted state is a single line of text (`last_announced_version`) under
|
|
11
|
+
* `getMagicContextStorageDir()`. OpenCode and Pi share the same file because
|
|
12
|
+
* they share the same storage root — so dismissing in one harness suppresses
|
|
13
|
+
* the dialog in the other for the same announcement.
|
|
14
|
+
*
|
|
15
|
+
* Leave both empty (`""` and `[]`) to skip the dialog entirely.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Bump only when there are user-visible changes worth a startup dialog.
|
|
19
|
+
* Does NOT need to match the published package version.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ANNOUNCEMENT_VERSION = "0.21.7";
|
|
22
|
+
/**
|
|
23
|
+
* Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
|
|
24
|
+
* TUI dialog renders cleanly without horizontal scroll on a typical terminal.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ANNOUNCEMENT_FEATURES: ReadonlyArray<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Persistent footer rendered below the version-specific bullets in every
|
|
29
|
+
* announcement. Stays in place across releases so users always see the Discord
|
|
30
|
+
* invite without us needing to repeat it in `ANNOUNCEMENT_FEATURES` each time.
|
|
31
|
+
*
|
|
32
|
+
* Leave empty (`""`) to suppress the footer.
|
|
33
|
+
*/
|
|
34
|
+
export declare const ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
|
|
35
|
+
/**
|
|
36
|
+
* Read the most recently dismissed announcement version, or `""` if none.
|
|
37
|
+
*
|
|
38
|
+
* Best-effort: any read failure returns `""` (which forces the announcement to
|
|
39
|
+
* re-show). The cost of a spurious second dialog is much smaller than the cost
|
|
40
|
+
* of suppressing a real announcement due to a transient FS error.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readLastAnnouncedVersion(): string;
|
|
43
|
+
/**
|
|
44
|
+
* Persist `version` as the most recently dismissed announcement. Best-effort:
|
|
45
|
+
* write failures are swallowed so dialog-confirm flows never throw on storage
|
|
46
|
+
* errors. Worst case the user sees the same dialog once more on next startup.
|
|
47
|
+
*/
|
|
48
|
+
export declare function markAnnouncementSeen(version: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* True when the configured `ANNOUNCEMENT_VERSION` has not yet been dismissed
|
|
51
|
+
* AND there is at least one feature to show. Used by both the TUI dialog path
|
|
52
|
+
* and the Desktop ignored-message fallback.
|
|
53
|
+
*/
|
|
54
|
+
export declare function shouldShowAnnouncement(): boolean;
|
|
55
|
+
//# sourceMappingURL=announcement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../../src/shared/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,MAAM,CAOvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,qDAAqD,CAAC;AAQtF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAQjD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAS1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAGhD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format an execute-threshold percentage for human-facing display.
|
|
3
|
+
*
|
|
4
|
+
* `executeThreshold` in the snapshot is always a percentage number, but it
|
|
5
|
+
* comes from two very different config paths:
|
|
6
|
+
* 1. `execute_threshold_percentage` (or its model-keyed variant) — user
|
|
7
|
+
* configures an integer like 65 directly. We must render exactly that.
|
|
8
|
+
* 2. `execute_threshold_tokens` — user configures absolute token cap (e.g.
|
|
9
|
+
* 128000). The resolver in `event-resolvers.ts` divides that by the
|
|
10
|
+
* model's context limit (`(128000 / 907788) * 100`) and the result is
|
|
11
|
+
* a long float like `14.099783080260304` that overflows the TUI cell
|
|
12
|
+
* (issue #90).
|
|
13
|
+
*
|
|
14
|
+
* Behaviour:
|
|
15
|
+
* - Integer input (≤0.05 fractional drift) renders without decimals.
|
|
16
|
+
* - Anything else is rendered with one decimal digit, which is precise
|
|
17
|
+
* enough to convey the configured token budget without smearing across
|
|
18
|
+
* two lines in a narrow sidebar.
|
|
19
|
+
*
|
|
20
|
+
* Returns the formatted percentage WITHOUT the trailing `%` so callers can
|
|
21
|
+
* compose richer strings like `47.5% / 65%` consistently.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatThresholdPercent(value: number | undefined | null): string;
|
|
24
|
+
//# sourceMappingURL=format-threshold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-threshold.d.ts","sourceRoot":"","sources":["../../src/shared/format-threshold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAK/E"}
|
|
@@ -17,6 +17,20 @@ export interface TuiMessage {
|
|
|
17
17
|
payload: Record<string, unknown>;
|
|
18
18
|
sessionId?: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Fetch the current startup announcement from the server, if any.
|
|
22
|
+
* Returns `{show: false}` when there's nothing to announce or when the
|
|
23
|
+
* configured ANNOUNCEMENT_VERSION has already been dismissed.
|
|
24
|
+
*/
|
|
25
|
+
export interface AnnouncementResponse {
|
|
26
|
+
show: boolean;
|
|
27
|
+
version?: string;
|
|
28
|
+
features?: string[];
|
|
29
|
+
footer?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function getAnnouncement(): Promise<AnnouncementResponse>;
|
|
32
|
+
/** Mark the current ANNOUNCEMENT_VERSION as dismissed on the server. */
|
|
33
|
+
export declare function markAnnounced(): Promise<boolean>;
|
|
20
34
|
/** Poll for pending server→TUI notifications via RPC. */
|
|
21
35
|
export declare function consumeTuiMessages(): Promise<TuiMessage[]>;
|
|
22
36
|
//# sourceMappingURL=context-db.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui/data/context-db.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0B,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpG,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAc9C,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAI/B;AAwFD,sDAAsD;AACtD,wBAAsB,mBAAmB,CACrC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED,wDAAwD;AACxD,wBAAsB,gBAAgB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,qCAAqC;AACrC,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvE;AAED,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAqBhE"}
|
|
1
|
+
{"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui/data/context-db.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0B,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpG,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAc9C,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAI/B;AAwFD,sDAAsD;AACtD,wBAAsB,mBAAmB,CACrC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED,wDAAwD;AACxD,wBAAsB,gBAAgB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,qCAAqC;AACrC,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvE;AAED,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAkBrE;AAED,wEAAwE;AACxE,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAQtD;AAED,yDAAyD;AACzD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAqBhE"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `announcement.ts` reads/writes a single `last_announced_version` file under
|
|
8
|
+
* `getMagicContextStorageDir()`. The behavior we test:
|
|
9
|
+
* 1. `markAnnouncementSeen` then `readLastAnnouncedVersion` round-trips
|
|
10
|
+
* 2. `shouldShowAnnouncement` returns false after a matching mark
|
|
11
|
+
* 3. `shouldShowAnnouncement` returns true after a non-matching mark
|
|
12
|
+
* 4. Empty-version inputs are no-ops (don't crash, don't write garbage)
|
|
13
|
+
*
|
|
14
|
+
* We isolate writes by pointing `XDG_DATA_HOME` at a temp dir before requiring
|
|
15
|
+
* the module fresh per test, since the module captures the storage path at
|
|
16
|
+
* import time via `getMagicContextStorageDir()`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let tmpRoot = "";
|
|
20
|
+
let originalXdg: string | undefined;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mc-announcement-test-"));
|
|
24
|
+
originalXdg = process.env.XDG_DATA_HOME;
|
|
25
|
+
process.env.XDG_DATA_HOME = tmpRoot;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
if (originalXdg === undefined) {
|
|
30
|
+
delete process.env.XDG_DATA_HOME;
|
|
31
|
+
} else {
|
|
32
|
+
process.env.XDG_DATA_HOME = originalXdg;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
36
|
+
} catch {
|
|
37
|
+
// best-effort
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("announcement state persistence", () => {
|
|
42
|
+
test("round-trips a dismissed version through the file", async () => {
|
|
43
|
+
// Fresh import after XDG override so the module captures the temp path
|
|
44
|
+
const mod = await import(`./announcement?t=${Date.now()}-rt`);
|
|
45
|
+
const { readLastAnnouncedVersion, markAnnouncementSeen } = mod;
|
|
46
|
+
|
|
47
|
+
expect(readLastAnnouncedVersion()).toBe("");
|
|
48
|
+
|
|
49
|
+
markAnnouncementSeen("9.9.9");
|
|
50
|
+
expect(readLastAnnouncedVersion()).toBe("9.9.9");
|
|
51
|
+
|
|
52
|
+
markAnnouncementSeen("9.9.10");
|
|
53
|
+
expect(readLastAnnouncedVersion()).toBe("9.9.10");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("ignores empty / zero-length version marks", async () => {
|
|
57
|
+
const mod = await import(`./announcement?t=${Date.now()}-empty`);
|
|
58
|
+
const { readLastAnnouncedVersion, markAnnouncementSeen } = mod;
|
|
59
|
+
|
|
60
|
+
markAnnouncementSeen("");
|
|
61
|
+
expect(readLastAnnouncedVersion()).toBe("");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("creates the storage directory if it does not exist", async () => {
|
|
65
|
+
const mod = await import(`./announcement?t=${Date.now()}-mkdir`);
|
|
66
|
+
const { markAnnouncementSeen } = mod;
|
|
67
|
+
|
|
68
|
+
// Storage dir lives under tmpRoot + cortexkit/magic-context — does not
|
|
69
|
+
// exist yet at the start of the test
|
|
70
|
+
const expectedDir = path.join(tmpRoot, "cortexkit", "magic-context");
|
|
71
|
+
expect(fs.existsSync(expectedDir)).toBe(false);
|
|
72
|
+
|
|
73
|
+
markAnnouncementSeen("0.21.7");
|
|
74
|
+
|
|
75
|
+
expect(fs.existsSync(expectedDir)).toBe(true);
|
|
76
|
+
expect(fs.readFileSync(path.join(expectedDir, "last_announced_version"), "utf-8")).toBe(
|
|
77
|
+
"0.21.7",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("trims whitespace from stored version on read", async () => {
|
|
82
|
+
const mod = await import(`./announcement?t=${Date.now()}-trim`);
|
|
83
|
+
const { readLastAnnouncedVersion } = mod;
|
|
84
|
+
|
|
85
|
+
const dir = path.join(tmpRoot, "cortexkit", "magic-context");
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
fs.writeFileSync(path.join(dir, "last_announced_version"), " 1.2.3 \n");
|
|
88
|
+
|
|
89
|
+
expect(readLastAnnouncedVersion()).toBe("1.2.3");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("shouldShowAnnouncement gating", () => {
|
|
94
|
+
test("returns false when the live version is already marked", async () => {
|
|
95
|
+
const mod = await import(`./announcement?t=${Date.now()}-match`);
|
|
96
|
+
const {
|
|
97
|
+
ANNOUNCEMENT_VERSION,
|
|
98
|
+
ANNOUNCEMENT_FEATURES,
|
|
99
|
+
markAnnouncementSeen,
|
|
100
|
+
shouldShowAnnouncement,
|
|
101
|
+
} = mod;
|
|
102
|
+
|
|
103
|
+
// Skip the test if announcements are currently disabled (empty constants)
|
|
104
|
+
// — the gate's empty-input behavior is covered separately below.
|
|
105
|
+
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
markAnnouncementSeen(ANNOUNCEMENT_VERSION);
|
|
110
|
+
expect(shouldShowAnnouncement()).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns true when the live version was never marked", async () => {
|
|
114
|
+
const mod = await import(`./announcement?t=${Date.now()}-none`);
|
|
115
|
+
const { ANNOUNCEMENT_VERSION, ANNOUNCEMENT_FEATURES, shouldShowAnnouncement } = mod;
|
|
116
|
+
|
|
117
|
+
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) {
|
|
118
|
+
// When empty, the gate is always false regardless of state
|
|
119
|
+
expect(shouldShowAnnouncement()).toBe(false);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// No mark has been written in this fresh tmpRoot, so the gate is open
|
|
124
|
+
expect(shouldShowAnnouncement()).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("returns true when a different (older) version is marked", async () => {
|
|
128
|
+
const mod = await import(`./announcement?t=${Date.now()}-older`);
|
|
129
|
+
const {
|
|
130
|
+
ANNOUNCEMENT_VERSION,
|
|
131
|
+
ANNOUNCEMENT_FEATURES,
|
|
132
|
+
markAnnouncementSeen,
|
|
133
|
+
shouldShowAnnouncement,
|
|
134
|
+
} = mod;
|
|
135
|
+
|
|
136
|
+
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
markAnnouncementSeen("0.0.0-pre-historic");
|
|
141
|
+
expect(shouldShowAnnouncement()).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-notes startup announcement shared by OpenCode plugin and Pi plugin.
|
|
3
|
+
*
|
|
4
|
+
* Bump `ANNOUNCEMENT_VERSION` and populate `ANNOUNCEMENT_FEATURES` *only* when a
|
|
5
|
+
* release ships user-facing news worth surfacing once at startup. Patch releases
|
|
6
|
+
* with no user-visible changes should leave both untouched — that way a user who
|
|
7
|
+
* already dismissed the dialog for the current `ANNOUNCEMENT_VERSION` won't see
|
|
8
|
+
* it again on the next bugfix bump.
|
|
9
|
+
*
|
|
10
|
+
* The persisted state is a single line of text (`last_announced_version`) under
|
|
11
|
+
* `getMagicContextStorageDir()`. OpenCode and Pi share the same file because
|
|
12
|
+
* they share the same storage root — so dismissing in one harness suppresses
|
|
13
|
+
* the dialog in the other for the same announcement.
|
|
14
|
+
*
|
|
15
|
+
* Leave both empty (`""` and `[]`) to skip the dialog entirely.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { getMagicContextStorageDir } from "./data-path";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bump only when there are user-visible changes worth a startup dialog.
|
|
24
|
+
* Does NOT need to match the published package version.
|
|
25
|
+
*/
|
|
26
|
+
export const ANNOUNCEMENT_VERSION = "0.21.7";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Short, user-facing bullet strings. Keep each line ~80 chars or shorter so the
|
|
30
|
+
* TUI dialog renders cleanly without horizontal scroll on a typical terminal.
|
|
31
|
+
*/
|
|
32
|
+
export const ANNOUNCEMENT_FEATURES: ReadonlyArray<string> = [
|
|
33
|
+
"Pi parity sweep: 44 audit findings fixed, including a critical SHIP-BLOCKER where /ctx-flush did not drain the pending Pi compaction queue.",
|
|
34
|
+
"Pi historian recovery fix: empty/no-op historian returns now clear emergency recovery so sessions cannot loop forever at 95%.",
|
|
35
|
+
"trimPiMessagesToBoundary now sweeps non-contiguous tool-result orphans, fixing provider 400s after compaction in long Pi sessions.",
|
|
36
|
+
"Hidden subagent tool isolation: historian, dreamer, and sidekick can no longer spawn subagents or run unsafe tools.",
|
|
37
|
+
"TUI sidebar and /ctx-status header now show execute threshold inline: '47.5% / 65%' on the left, '475K / 1.0M' on the right.",
|
|
38
|
+
"doctor --issue now caps GitHub issue bodies at ~60KB with a dedicated 'Recent errors' section so reports stay submittable.",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Persistent footer rendered below the version-specific bullets in every
|
|
43
|
+
* announcement. Stays in place across releases so users always see the Discord
|
|
44
|
+
* invite without us needing to repeat it in `ANNOUNCEMENT_FEATURES` each time.
|
|
45
|
+
*
|
|
46
|
+
* Leave empty (`""`) to suppress the footer.
|
|
47
|
+
*/
|
|
48
|
+
export const ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
|
|
49
|
+
|
|
50
|
+
const STATE_FILENAME = "last_announced_version";
|
|
51
|
+
|
|
52
|
+
function getStateFilePath(): string {
|
|
53
|
+
return path.join(getMagicContextStorageDir(), STATE_FILENAME);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the most recently dismissed announcement version, or `""` if none.
|
|
58
|
+
*
|
|
59
|
+
* Best-effort: any read failure returns `""` (which forces the announcement to
|
|
60
|
+
* re-show). The cost of a spurious second dialog is much smaller than the cost
|
|
61
|
+
* of suppressing a real announcement due to a transient FS error.
|
|
62
|
+
*/
|
|
63
|
+
export function readLastAnnouncedVersion(): string {
|
|
64
|
+
try {
|
|
65
|
+
const file = getStateFilePath();
|
|
66
|
+
if (!fs.existsSync(file)) return "";
|
|
67
|
+
return fs.readFileSync(file, "utf-8").trim();
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Persist `version` as the most recently dismissed announcement. Best-effort:
|
|
75
|
+
* write failures are swallowed so dialog-confirm flows never throw on storage
|
|
76
|
+
* errors. Worst case the user sees the same dialog once more on next startup.
|
|
77
|
+
*/
|
|
78
|
+
export function markAnnouncementSeen(version: string): void {
|
|
79
|
+
if (!version) return;
|
|
80
|
+
try {
|
|
81
|
+
const dir = getMagicContextStorageDir();
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(getStateFilePath(), version);
|
|
84
|
+
} catch {
|
|
85
|
+
// best-effort
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* True when the configured `ANNOUNCEMENT_VERSION` has not yet been dismissed
|
|
91
|
+
* AND there is at least one feature to show. Used by both the TUI dialog path
|
|
92
|
+
* and the Desktop ignored-message fallback.
|
|
93
|
+
*/
|
|
94
|
+
export function shouldShowAnnouncement(): boolean {
|
|
95
|
+
if (!ANNOUNCEMENT_VERSION || ANNOUNCEMENT_FEATURES.length === 0) return false;
|
|
96
|
+
return readLastAnnouncedVersion() !== ANNOUNCEMENT_VERSION;
|
|
97
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format an execute-threshold percentage for human-facing display.
|
|
3
|
+
*
|
|
4
|
+
* `executeThreshold` in the snapshot is always a percentage number, but it
|
|
5
|
+
* comes from two very different config paths:
|
|
6
|
+
* 1. `execute_threshold_percentage` (or its model-keyed variant) — user
|
|
7
|
+
* configures an integer like 65 directly. We must render exactly that.
|
|
8
|
+
* 2. `execute_threshold_tokens` — user configures absolute token cap (e.g.
|
|
9
|
+
* 128000). The resolver in `event-resolvers.ts` divides that by the
|
|
10
|
+
* model's context limit (`(128000 / 907788) * 100`) and the result is
|
|
11
|
+
* a long float like `14.099783080260304` that overflows the TUI cell
|
|
12
|
+
* (issue #90).
|
|
13
|
+
*
|
|
14
|
+
* Behaviour:
|
|
15
|
+
* - Integer input (≤0.05 fractional drift) renders without decimals.
|
|
16
|
+
* - Anything else is rendered with one decimal digit, which is precise
|
|
17
|
+
* enough to convey the configured token budget without smearing across
|
|
18
|
+
* two lines in a narrow sidebar.
|
|
19
|
+
*
|
|
20
|
+
* Returns the formatted percentage WITHOUT the trailing `%` so callers can
|
|
21
|
+
* compose richer strings like `47.5% / 65%` consistently.
|
|
22
|
+
*/
|
|
23
|
+
export function formatThresholdPercent(value: number | undefined | null): string {
|
|
24
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
25
|
+
const rounded = Math.round(value);
|
|
26
|
+
if (Math.abs(value - rounded) < 0.05) return String(rounded);
|
|
27
|
+
return value.toFixed(1);
|
|
28
|
+
}
|
|
@@ -233,6 +233,49 @@ export interface TuiMessage {
|
|
|
233
233
|
sessionId?: string;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Fetch the current startup announcement from the server, if any.
|
|
238
|
+
* Returns `{show: false}` when there's nothing to announce or when the
|
|
239
|
+
* configured ANNOUNCEMENT_VERSION has already been dismissed.
|
|
240
|
+
*/
|
|
241
|
+
export interface AnnouncementResponse {
|
|
242
|
+
show: boolean;
|
|
243
|
+
version?: string;
|
|
244
|
+
features?: string[];
|
|
245
|
+
footer?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function getAnnouncement(): Promise<AnnouncementResponse> {
|
|
249
|
+
if (!rpcClient) return { show: false };
|
|
250
|
+
try {
|
|
251
|
+
const result = await rpcClient.call<{
|
|
252
|
+
show?: boolean;
|
|
253
|
+
version?: string;
|
|
254
|
+
features?: string[];
|
|
255
|
+
footer?: string;
|
|
256
|
+
}>("get-announcement", {});
|
|
257
|
+
return {
|
|
258
|
+
show: result.show === true,
|
|
259
|
+
version: result.version,
|
|
260
|
+
features: Array.isArray(result.features) ? result.features : undefined,
|
|
261
|
+
footer: typeof result.footer === "string" ? result.footer : undefined,
|
|
262
|
+
};
|
|
263
|
+
} catch {
|
|
264
|
+
return { show: false };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Mark the current ANNOUNCEMENT_VERSION as dismissed on the server. */
|
|
269
|
+
export async function markAnnounced(): Promise<boolean> {
|
|
270
|
+
if (!rpcClient) return false;
|
|
271
|
+
try {
|
|
272
|
+
const result = await rpcClient.call<{ ok?: boolean }>("mark-announced", {});
|
|
273
|
+
return result.ok === true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
236
279
|
/** Poll for pending server→TUI notifications via RPC. */
|
|
237
280
|
export async function consumeTuiMessages(): Promise<TuiMessage[]> {
|
|
238
281
|
if (!rpcClient) return [];
|
package/src/tui/index.tsx
CHANGED
|
@@ -6,7 +6,8 @@ import { createMemo } from "solid-js"
|
|
|
6
6
|
import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
7
7
|
import { createSidebarContentSlot } from "./slots/sidebar-content"
|
|
8
8
|
import packageJson from "../../package.json"
|
|
9
|
-
import { closeRpc, consumeTuiMessages, getCompartmentCount, initRpcClient, loadStatusDetail, requestRecomp, type StatusDetail } from "./data/context-db"
|
|
9
|
+
import { closeRpc, consumeTuiMessages, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
|
|
10
|
+
import { formatThresholdPercent } from "../shared/format-threshold"
|
|
10
11
|
import { detectConflicts } from "../shared/conflict-detector"
|
|
11
12
|
import { fixConflicts } from "../shared/conflict-fixer"
|
|
12
13
|
import { readJsoncFile } from "../shared/jsonc-parser"
|
|
@@ -296,7 +297,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
296
297
|
them how close they are to compaction triggering. */}
|
|
297
298
|
<box flexDirection="row" justifyContent="space-between" width="100%">
|
|
298
299
|
<text fg={s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent}>
|
|
299
|
-
<b>{s().usagePercentage.toFixed(1)}%</b> / {s().executeThreshold}%
|
|
300
|
+
<b>{s().usagePercentage.toFixed(1)}%</b> / {formatThresholdPercent(s().executeThreshold)}%
|
|
300
301
|
</text>
|
|
301
302
|
<text fg={s().usagePercentage >= 80 ? t().error : s().usagePercentage >= 65 ? t().warning : t().accent}>
|
|
302
303
|
{fmt(s().inputTokens)} / {contextLimit() > 0 ? fmt(contextLimit()) : "?"} tokens
|
|
@@ -341,7 +342,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
341
342
|
<R t={t()} l="Configured" v={s().cacheTtl} />
|
|
342
343
|
<R t={t()} l="Last response" v={s().lastResponseTime > 0 ? `${Math.round(elapsed() / 1000)}s ago` : "never"} />
|
|
343
344
|
<R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
|
|
344
|
-
<R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : `at TTL or ≥${s().executeThreshold}%`} fg={t().textMuted} />
|
|
345
|
+
<R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : `at TTL or ≥${formatThresholdPercent(s().executeThreshold)}%`} fg={t().textMuted} />
|
|
345
346
|
<box marginTop={1}>
|
|
346
347
|
<text fg={t().text}><b>Memory</b></text>
|
|
347
348
|
</box>
|
|
@@ -351,7 +352,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
351
352
|
{/* Right column */}
|
|
352
353
|
<box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
353
354
|
<text fg={t().text}><b>Rolling Nudges</b></text>
|
|
354
|
-
<R t={t()} l="Execute threshold" v={`${s().executeThreshold}%`} />
|
|
355
|
+
<R t={t()} l="Execute threshold" v={`${formatThresholdPercent(s().executeThreshold)}%`} />
|
|
355
356
|
<R t={t()} l="Nudge anchor" v={`${fmt(s().lastNudgeTokens)} tok`} />
|
|
356
357
|
<R t={t()} l="Interval" v={`${fmt(s().nudgeInterval)} tok`} fg={t().textMuted} />
|
|
357
358
|
<R t={t()} l="Next nudge after" v={`${fmt(s().nextNudgeAfter)} tok`} />
|
|
@@ -563,6 +564,63 @@ function registerCommandPaletteEntries(api: TuiPluginApi): void {
|
|
|
563
564
|
// via RPC.
|
|
564
565
|
}
|
|
565
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Show the one-shot "What's new" dialog on TUI startup if the server tells us
|
|
569
|
+
* to. The server is the source of truth: it has the version + features
|
|
570
|
+
* constants AND owns the persistence file. We just render and report back.
|
|
571
|
+
*
|
|
572
|
+
* Failure-tolerant by design — if the server isn't ready or the RPC fails,
|
|
573
|
+
* we silently skip (the next TUI launch will retry).
|
|
574
|
+
*/
|
|
575
|
+
/**
|
|
576
|
+
* URLs render as plain text. Modern terminals (iTerm2, kitty, WezTerm, Ghostty,
|
|
577
|
+
* recent macOS Terminal) auto-detect URLs and let users Cmd-click; older
|
|
578
|
+
* terminals require manual copy. We tried opentui's `<a href>` JSX intrinsic
|
|
579
|
+
* for application-level OSC 8 clickability, but it's a span-like element that
|
|
580
|
+
* forced text out of opentui's word-wrap mode, causing bullets to bleed past
|
|
581
|
+
* the dialog border. Pure-string children of `<text>` wrap correctly, so the
|
|
582
|
+
* AFT-style DialogAlert + plain string is the right surface here.
|
|
583
|
+
*/
|
|
584
|
+
async function showStartupAnnouncement(api: TuiPluginApi): Promise<void> {
|
|
585
|
+
try {
|
|
586
|
+
const ann = await getAnnouncement()
|
|
587
|
+
if (!ann.show || !ann.version || !ann.features || ann.features.length === 0) return
|
|
588
|
+
|
|
589
|
+
const title = `Magic Context v${ann.version}`
|
|
590
|
+
const lines: string[] = [
|
|
591
|
+
"What's new:",
|
|
592
|
+
"",
|
|
593
|
+
...ann.features.map((line) => ` • ${line}`),
|
|
594
|
+
]
|
|
595
|
+
if (ann.footer && ann.footer.trim().length > 0) {
|
|
596
|
+
// Blank-line separator keeps the persistent footer (Discord invite,
|
|
597
|
+
// etc.) visually distinct from the version-specific bullets.
|
|
598
|
+
lines.push("", ann.footer)
|
|
599
|
+
}
|
|
600
|
+
const message = lines.join("\n")
|
|
601
|
+
|
|
602
|
+
api.ui.dialog.replace(
|
|
603
|
+
() => (
|
|
604
|
+
<api.ui.DialogAlert
|
|
605
|
+
title={title}
|
|
606
|
+
message={message}
|
|
607
|
+
onConfirm={() => {
|
|
608
|
+
void markAnnounced()
|
|
609
|
+
}}
|
|
610
|
+
/>
|
|
611
|
+
),
|
|
612
|
+
() => {
|
|
613
|
+
// User dismissed via Escape rather than confirming. Mark
|
|
614
|
+
// dismissed anyway — they saw the dialog, that's the contract.
|
|
615
|
+
void markAnnounced()
|
|
616
|
+
},
|
|
617
|
+
)
|
|
618
|
+
} catch {
|
|
619
|
+
// RPC not ready yet (port file missing or transient HTTP failure) —
|
|
620
|
+
// silently skip. The next TUI start re-checks.
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
566
624
|
const tui: TuiPlugin = async (api, _options, meta) => {
|
|
567
625
|
// Initialize RPC client for server communication
|
|
568
626
|
const directory = api.state.path.directory ?? ""
|
|
@@ -623,6 +681,12 @@ const tui: TuiPlugin = async (api, _options, meta) => {
|
|
|
623
681
|
return
|
|
624
682
|
}
|
|
625
683
|
|
|
684
|
+
// Show one-shot release announcement after conflict gate.
|
|
685
|
+
// Fire-and-forget: if the server isn't ready or RPC fails, the next TUI
|
|
686
|
+
// launch will retry. Dialog only appears once per ANNOUNCEMENT_VERSION
|
|
687
|
+
// (persisted via mark-announced RPC writing last_announced_version).
|
|
688
|
+
void showStartupAnnouncement(api)
|
|
689
|
+
|
|
626
690
|
// Note: if TUI plugin is loaded, tui.json already has our entry.
|
|
627
691
|
// But if the user added it manually and later removes it, or if they
|
|
628
692
|
// use setup/doctor which handles tui.json, this code is already running.
|
|
@@ -3,6 +3,7 @@ import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
|
|
3
3
|
import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
4
|
import packageJson from "../../../package.json"
|
|
5
5
|
import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
|
|
6
|
+
import { formatThresholdPercent } from "../../shared/format-threshold"
|
|
6
7
|
|
|
7
8
|
const SINGLE_BORDER = { type: "single" } as any
|
|
8
9
|
const REFRESH_DEBOUNCE_MS = 150
|
|
@@ -47,7 +48,15 @@ const TokenBreakdown = (props: {
|
|
|
47
48
|
theme: TuiThemeCurrent
|
|
48
49
|
snapshot: SidebarSnapshot
|
|
49
50
|
}) => {
|
|
50
|
-
|
|
51
|
+
// Bar width is hardcoded because the @opencode-ai/plugin/tui slot API does
|
|
52
|
+
// not expose the rendered sidebar width to plugins. 24 chars is the safe
|
|
53
|
+
// floor that fits every realistic sidebar configuration we've observed —
|
|
54
|
+
// OpenCode TUI's sidebar narrows with the terminal, and 36 (the previous
|
|
55
|
+
// value) overflowed users' actual layouts (issue #90), wrapping the bar
|
|
56
|
+
// onto a second line. If/when the slot API surfaces a real width, this
|
|
57
|
+
// should become Math.max(20, providedWidth) like the Pi status dialog
|
|
58
|
+
// already does (`Math.max(20, innerWidth)`).
|
|
59
|
+
const barWidth = 24
|
|
51
60
|
|
|
52
61
|
const segments = createMemo<TokenSegment[]>(() => {
|
|
53
62
|
const s = props.snapshot
|
|
@@ -370,7 +379,7 @@ const SidebarContent = (props: {
|
|
|
370
379
|
"47.5% / 65%" tells the user how close they
|
|
371
380
|
are to the next compaction trigger. */}
|
|
372
381
|
<text fg={contextSummaryColor()}>
|
|
373
|
-
<b>{s()!.usagePercentage.toFixed(1)}%</b> / {s()!.executeThreshold}%
|
|
382
|
+
<b>{s()!.usagePercentage.toFixed(1)}%</b> / {formatThresholdPercent(s()!.executeThreshold)}%
|
|
374
383
|
</text>
|
|
375
384
|
{/* Right: absolute token usage vs the model's
|
|
376
385
|
full context window (separate from the
|