@moxxy/sdk 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/elision-state.d.ts.map +1 -1
  2. package/dist/elision-state.js +6 -0
  3. package/dist/elision-state.js.map +1 -1
  4. package/dist/events.d.ts +33 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/index.d.ts +8 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/mode-helpers.d.ts +12 -0
  11. package/dist/mode-helpers.d.ts.map +1 -1
  12. package/dist/mode-helpers.js +105 -3
  13. package/dist/mode-helpers.js.map +1 -1
  14. package/dist/mode.d.ts +9 -0
  15. package/dist/mode.d.ts.map +1 -1
  16. package/dist/mode.js.map +1 -1
  17. package/dist/plugin.d.ts +10 -1
  18. package/dist/plugin.d.ts.map +1 -1
  19. package/dist/provider-login-bridge.d.ts +53 -0
  20. package/dist/provider-login-bridge.d.ts.map +1 -0
  21. package/dist/provider-login-bridge.js +95 -0
  22. package/dist/provider-login-bridge.js.map +1 -0
  23. package/dist/provider.d.ts +53 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/session-like.d.ts +36 -0
  26. package/dist/session-like.d.ts.map +1 -1
  27. package/dist/subagent.d.ts +6 -0
  28. package/dist/subagent.d.ts.map +1 -1
  29. package/dist/surface.d.ts +159 -0
  30. package/dist/surface.d.ts.map +1 -0
  31. package/dist/surface.js +27 -0
  32. package/dist/surface.js.map +1 -0
  33. package/dist/tool-display.d.ts +73 -0
  34. package/dist/tool-display.d.ts.map +1 -0
  35. package/dist/tool-display.js +66 -0
  36. package/dist/tool-display.js.map +1 -0
  37. package/package.json +5 -1
  38. package/src/elision-state.ts +5 -0
  39. package/src/events.ts +36 -0
  40. package/src/index.ts +43 -0
  41. package/src/loop-helpers.test.ts +40 -0
  42. package/src/mode-helpers.ts +114 -3
  43. package/src/mode.ts +7 -0
  44. package/src/plugin.ts +10 -1
  45. package/src/provider-login-bridge.test.ts +78 -0
  46. package/src/provider-login-bridge.ts +105 -0
  47. package/src/provider.ts +45 -2
  48. package/src/session-like.ts +38 -0
  49. package/src/subagent.ts +6 -0
  50. package/src/surface.ts +173 -0
  51. package/src/tool-display.test.ts +71 -0
  52. package/src/tool-display.ts +118 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Channel-agnostic "rich tool result" payloads.
3
+ *
4
+ * A tool whose result is more than a line of text (a file diff, eventually a
5
+ * table or chart) returns a `ToolDisplayResult`: a short `forModel` string the
6
+ * model sees, plus a structured `display` every channel can render natively.
7
+ * The projection layer (see `mode-helpers.ts`) sends ONLY `forModel` to the
8
+ * model, so the rich payload never bloats the context window.
9
+ *
10
+ * The first (and currently only) display kind is `file-diff`, emitted by the
11
+ * built-in Write/Edit tools. It carries the changed slices of a file (a few
12
+ * lines of context around each edit) — never the whole file — so the payload
13
+ * stays bounded regardless of file size. Channels render it as a classic diff:
14
+ * line numbers, +/- markers, green/red line backgrounds.
15
+ */
16
+ /** One rendered line of a diff. */
17
+ export interface DiffLine {
18
+ readonly kind: 'context' | 'add' | 'del';
19
+ readonly text: string;
20
+ /** 1-based line number in the OLD file (del + context lines). */
21
+ readonly oldNo?: number;
22
+ /** 1-based line number in the NEW file (add + context lines). */
23
+ readonly newNo?: number;
24
+ }
25
+ /** A contiguous changed region plus its surrounding context lines. */
26
+ export interface DiffHunk {
27
+ readonly oldStart: number;
28
+ readonly oldLines: number;
29
+ readonly newStart: number;
30
+ readonly newLines: number;
31
+ readonly lines: ReadonlyArray<DiffLine>;
32
+ }
33
+ /** Structured diff for a single file write/edit. */
34
+ export interface FileDiffDisplay {
35
+ readonly kind: 'file-diff';
36
+ /** Display path — relative to cwd when possible, else absolute. */
37
+ readonly path: string;
38
+ readonly mode: 'create' | 'update';
39
+ readonly added: number;
40
+ readonly removed: number;
41
+ readonly hunks: ReadonlyArray<DiffHunk>;
42
+ /** Set when a very large diff was capped (some hunks/lines dropped). */
43
+ readonly truncated?: boolean;
44
+ }
45
+ /** Extensible union of structured tool-result payloads. */
46
+ export type ToolDisplay = FileDiffDisplay;
47
+ /**
48
+ * The shape a tool returns when it wants a rich, channel-rendered result.
49
+ * `forModel` is the only thing the model sees; `display` is for channels.
50
+ */
51
+ export interface ToolDisplayResult {
52
+ readonly forModel: string;
53
+ readonly display: ToolDisplay;
54
+ }
55
+ export declare function isToolDisplayResult(x: unknown): x is ToolDisplayResult;
56
+ export declare function isToolDisplay(x: unknown): x is ToolDisplay;
57
+ export declare function isFileDiffDisplay(x: unknown): x is FileDiffDisplay;
58
+ /** Human summary, e.g. "Added 10 lines, removed 1 line". */
59
+ export declare function fileDiffSummary(d: FileDiffDisplay): string;
60
+ /** Verb for the diff header — "Create" for new files, else "Update". */
61
+ export declare function fileDiffVerb(d: FileDiffDisplay): string;
62
+ /** Gutter number a renderer should show for a line (new number, or old for deletions). */
63
+ export declare function diffGutterNo(line: DiffLine): number | undefined;
64
+ /** A renderable row: either a diff line or a gap marker between hunks. */
65
+ export type DiffRow = DiffLine | {
66
+ readonly kind: 'gap';
67
+ };
68
+ /**
69
+ * Flatten a file diff's hunks into a single row list, inserting a `gap` marker
70
+ * between non-contiguous hunks (channels render it as a `⋯` separator).
71
+ */
72
+ export declare function toDiffRows(d: FileDiffDisplay): DiffRow[];
73
+ //# sourceMappingURL=tool-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-display.d.ts","sourceRoot":"","sources":["../src/tool-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mCAAmC;AACnC,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,sEAAsE;AACtE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,2DAA2D;AAC3D,MAAM,MAAM,WAAW,GAAG,eAAe,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,iBAAiB,CAOtE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAE1D;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,eAAe,CAOlE;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAQ1D;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAEvD;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED,0EAA0E;AAC1E,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,EAAE,CAOxD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Channel-agnostic "rich tool result" payloads.
3
+ *
4
+ * A tool whose result is more than a line of text (a file diff, eventually a
5
+ * table or chart) returns a `ToolDisplayResult`: a short `forModel` string the
6
+ * model sees, plus a structured `display` every channel can render natively.
7
+ * The projection layer (see `mode-helpers.ts`) sends ONLY `forModel` to the
8
+ * model, so the rich payload never bloats the context window.
9
+ *
10
+ * The first (and currently only) display kind is `file-diff`, emitted by the
11
+ * built-in Write/Edit tools. It carries the changed slices of a file (a few
12
+ * lines of context around each edit) — never the whole file — so the payload
13
+ * stays bounded regardless of file size. Channels render it as a classic diff:
14
+ * line numbers, +/- markers, green/red line backgrounds.
15
+ */
16
+ export function isToolDisplayResult(x) {
17
+ return (typeof x === 'object' &&
18
+ x !== null &&
19
+ typeof x.forModel === 'string' &&
20
+ isToolDisplay(x.display));
21
+ }
22
+ export function isToolDisplay(x) {
23
+ return isFileDiffDisplay(x);
24
+ }
25
+ export function isFileDiffDisplay(x) {
26
+ return (typeof x === 'object' &&
27
+ x !== null &&
28
+ x.kind === 'file-diff' &&
29
+ Array.isArray(x.hunks));
30
+ }
31
+ /** Human summary, e.g. "Added 10 lines, removed 1 line". */
32
+ export function fileDiffSummary(d) {
33
+ const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
34
+ const parts = [];
35
+ if (d.added > 0 || d.removed === 0)
36
+ parts.push(`Added ${plural(d.added, 'line')}`);
37
+ if (d.removed > 0)
38
+ parts.push(`${parts.length ? 'removed' : 'Removed'} ${plural(d.removed, 'line')}`);
39
+ let summary = parts.join(', ');
40
+ if (d.truncated)
41
+ summary += ' (diff truncated)';
42
+ return summary;
43
+ }
44
+ /** Verb for the diff header — "Create" for new files, else "Update". */
45
+ export function fileDiffVerb(d) {
46
+ return d.mode === 'create' ? 'Create' : 'Update';
47
+ }
48
+ /** Gutter number a renderer should show for a line (new number, or old for deletions). */
49
+ export function diffGutterNo(line) {
50
+ return line.kind === 'del' ? line.oldNo : line.newNo;
51
+ }
52
+ /**
53
+ * Flatten a file diff's hunks into a single row list, inserting a `gap` marker
54
+ * between non-contiguous hunks (channels render it as a `⋯` separator).
55
+ */
56
+ export function toDiffRows(d) {
57
+ const rows = [];
58
+ d.hunks.forEach((hunk, i) => {
59
+ if (i > 0)
60
+ rows.push({ kind: 'gap' });
61
+ for (const line of hunk.lines)
62
+ rows.push(line);
63
+ });
64
+ return rows;
65
+ }
66
+ //# sourceMappingURL=tool-display.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-display.js","sourceRoot":"","sources":["../src/tool-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA8CH,MAAM,UAAU,mBAAmB,CAAC,CAAU;IAC5C,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACV,OAAQ,CAA4B,CAAC,QAAQ,KAAK,QAAQ;QAC1D,aAAa,CAAE,CAA2B,CAAC,OAAO,CAAC,CACpD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAU;IACtC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACT,CAAwB,CAAC,IAAI,KAAK,WAAW;QAC9C,KAAK,CAAC,OAAO,CAAE,CAAyB,CAAC,KAAK,CAAC,CAChD,CAAC;AACJ,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,CAAkB;IAChD,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAClF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACtG,IAAI,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,CAAC,SAAS;QAAE,OAAO,IAAI,mBAAmB,CAAC;IAChD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,CAAkB;IAC7C,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AACnD,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,OAAO,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;AACvD,CAAC;AAKD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,CAAkB;IAC3C,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC1B,IAAI,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Typed public surface for the moxxy framework: event types, define* factories, lifecycle hook signatures.",
5
5
  "keywords": [
6
6
  "moxxy",
@@ -44,6 +44,10 @@
44
44
  ".": {
45
45
  "types": "./dist/index.d.ts",
46
46
  "import": "./dist/index.js"
47
+ },
48
+ "./tool-display": {
49
+ "types": "./dist/tool-display.d.ts",
50
+ "import": "./dist/tool-display.js"
47
51
  }
48
52
  },
49
53
  "files": [
@@ -1,4 +1,5 @@
1
1
  import type { MoxxyEvent } from './events.js';
2
+ import { isToolDisplayResult } from './tool-display.js';
2
3
 
3
4
  /**
4
5
  * Shared elision decision logic — the single source of truth for "is this event
@@ -16,6 +17,10 @@ export const TINY_TURN_CHARS = 200;
16
17
  export function toolResultBytes(output: unknown, errorMessage?: string): number {
17
18
  if (errorMessage !== undefined) return errorMessage.length;
18
19
  if (typeof output === 'string') return output.length;
20
+ // Rich results (e.g. file diffs) only ever send their short `forModel`
21
+ // string to the model — measure THAT so the estimate matches projection
22
+ // and the bulky `display` payload never trips elision.
23
+ if (isToolDisplayResult(output)) return output.forModel.length;
19
24
  try {
20
25
  return JSON.stringify(output ?? '').length;
21
26
  } catch {
package/src/events.ts CHANGED
@@ -49,6 +49,40 @@ export interface AssistantMessageEvent extends EventBase {
49
49
  readonly stopReason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error';
50
50
  }
51
51
 
52
+ /**
53
+ * A live reasoning/thinking delta. Parallels {@link AssistantChunkEvent}: it is
54
+ * streamed for the UI's live "thinking…" preview and is NOT retained in the
55
+ * display log (renderers accumulate it ephemerally, clearing on the matching
56
+ * {@link ReasoningMessageEvent} or the turn's `assistant_message`). Only
57
+ * emitted when the active provider+model supports reasoning AND it is enabled
58
+ * in that provider's config.
59
+ */
60
+ export interface ReasoningChunkEvent extends EventBase {
61
+ readonly type: 'reasoning_chunk';
62
+ readonly delta: string;
63
+ }
64
+
65
+ /**
66
+ * A finalized reasoning summary for ONE provider call. Parallels
67
+ * {@link AssistantMessageEvent}: it is persisted + replayed, and because
68
+ * `collectProviderStream` runs once per loop round these land between tool
69
+ * batches (the "summary of the work done between calls" behavior). The
70
+ * `signature`/`redacted`/`encrypted` fields exist purely for history
71
+ * round-trip — Anthropic requires the signed `thinking` block be replayed
72
+ * first on a tool-use continuation; Codex/Anthropic redacted thinking carries
73
+ * an opaque blob replayed verbatim and never shown.
74
+ */
75
+ export interface ReasoningMessageEvent extends EventBase {
76
+ readonly type: 'reasoning_message';
77
+ readonly content: string;
78
+ /** Anthropic thinking-block signature; absent for providers that don't sign. */
79
+ readonly signature?: string;
80
+ /** True when the reasoning is redacted/encrypted and must not be displayed. */
81
+ readonly redacted?: boolean;
82
+ /** Opaque provider blob (Anthropic redacted_thinking data / Codex encrypted_content) replayed as-is. */
83
+ readonly encrypted?: string;
84
+ }
85
+
52
86
  export interface ToolCallRequestedEvent extends EventBase {
53
87
  readonly type: 'tool_call_requested';
54
88
  readonly callId: ToolCallId;
@@ -226,6 +260,8 @@ export type MoxxyEvent =
226
260
  | UserPromptEvent
227
261
  | AssistantChunkEvent
228
262
  | AssistantMessageEvent
263
+ | ReasoningChunkEvent
264
+ | ReasoningMessageEvent
229
265
  | ToolCallRequestedEvent
230
266
  | ToolCallApprovedEvent
231
267
  | ToolCallDeniedEvent
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export type {
19
19
  UserPromptAttachment,
20
20
  AssistantChunkEvent,
21
21
  AssistantMessageEvent,
22
+ ReasoningChunkEvent,
23
+ ReasoningMessageEvent,
22
24
  ToolCallRequestedEvent,
23
25
  ToolCallApprovedEvent,
24
26
  ToolCallDeniedEvent,
@@ -50,6 +52,8 @@ export type {
50
52
  CredentialResolver,
51
53
  McpAdminView,
52
54
  McpServerStatusView,
55
+ ProviderAdminView,
56
+ ProviderConfigurePatch,
53
57
  WorkflowsView,
54
58
  WorkflowSummaryView,
55
59
  WorkflowRunView,
@@ -134,7 +138,30 @@ export type {
134
138
  ProviderOAuthStatus,
135
139
  ProviderAuthDescriptor,
136
140
  } from './provider.js';
141
+ export {
142
+ encodeLoginPrompt,
143
+ decodeLoginPrompt,
144
+ createLoginStreamScanner,
145
+ } from './provider-login-bridge.js';
146
+ export type { LoginPromptRequest, LoginStreamItem } from './provider-login-bridge.js';
137
147
  export type { CacheStrategyDef, CacheStrategyContext } from './cache-strategy.js';
148
+ export type {
149
+ DiffLine,
150
+ DiffHunk,
151
+ DiffRow,
152
+ FileDiffDisplay,
153
+ ToolDisplay,
154
+ ToolDisplayResult,
155
+ } from './tool-display.js';
156
+ export {
157
+ isToolDisplayResult,
158
+ isToolDisplay,
159
+ isFileDiffDisplay,
160
+ fileDiffSummary,
161
+ fileDiffVerb,
162
+ diffGutterNo,
163
+ toDiffRows,
164
+ } from './tool-display.js';
138
165
  export type {
139
166
  ViewNode,
140
167
  ViewAction,
@@ -332,6 +359,22 @@ export type {
332
359
  export type { EmbeddingProvider, EmbedderDef } from './embedding.js';
333
360
  export { CachedEmbeddingProvider } from './embedding-cache.js';
334
361
 
362
+ export { defineSurface } from './surface.js';
363
+ export type {
364
+ SurfaceKind,
365
+ SurfaceDataMessage,
366
+ SurfaceInputMessage,
367
+ SurfaceSize,
368
+ SurfaceInstance,
369
+ SurfaceAvailability,
370
+ SurfaceContext,
371
+ SurfaceDef,
372
+ SurfaceRegistry,
373
+ SurfaceInfo,
374
+ OpenSurfaceResult,
375
+ SurfaceHost,
376
+ } from './surface.js';
377
+
335
378
  export type {
336
379
  RequirementKind,
337
380
  RequirementState,
@@ -151,6 +151,46 @@ describe('projectMessagesFromLog', () => {
151
151
  content: [{ type: 'text', text: 'running it now' }],
152
152
  });
153
153
  });
154
+
155
+ it('projects only `forModel` for a rich tool result (file diff), never the display payload', () => {
156
+ // A Write/Edit returns { forModel, display:{kind:'file-diff', hunks:[…]} }.
157
+ // The model must see ONLY the short summary — the bulky hunks stay
158
+ // channel-side, so the context window never carries the full diff.
159
+ const display = {
160
+ forModel: 'edited /repo/a.ts — added 1 line, removed 1 line',
161
+ display: {
162
+ kind: 'file-diff' as const,
163
+ path: 'a.ts',
164
+ mode: 'update' as const,
165
+ added: 1,
166
+ removed: 1,
167
+ hunks: [
168
+ {
169
+ oldStart: 1,
170
+ oldLines: 1,
171
+ newStart: 1,
172
+ newLines: 1,
173
+ lines: [{ kind: 'del' as const, text: 'before', oldNo: 1 }, { kind: 'add' as const, text: 'after', newNo: 1 }],
174
+ },
175
+ ],
176
+ },
177
+ };
178
+ const log = reader([
179
+ event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'edit it' }),
180
+ event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: 'c1', name: 'Edit', input: {} }),
181
+ event(2, { type: 'tool_result', turnId: t1, source: 'tool', callId: 'c1', ok: true, output: display }),
182
+ ]);
183
+
184
+ const messages = projectMessagesFromLog({ log });
185
+ const toolResult = messages.find((m) => m.role === 'tool_result');
186
+ const block = toolResult!.content[0]!;
187
+ expect(block).toMatchObject({ type: 'tool_result', content: display.forModel });
188
+ // The hunks / file content must NOT leak into the projected text.
189
+ if (block.type === 'tool_result') {
190
+ expect(block.content).not.toContain('hunks');
191
+ expect(block.content).not.toContain('before');
192
+ }
193
+ });
154
194
  });
155
195
 
156
196
  function reader(events: ReadonlyArray<MoxxyEvent>): EventLogReader {
@@ -11,6 +11,7 @@ import {
11
11
  toolResultStub,
12
12
  toolResultStubbed,
13
13
  } from './elision-state.js';
14
+ import { isToolDisplayResult } from './tool-display.js';
14
15
  import { applyLazyTools } from './tool-gating.js';
15
16
  import { runCompactionIfNeeded } from './compactor-helpers.js';
16
17
  import { runElisionIfNeeded } from './elision-helpers.js';
@@ -196,6 +197,14 @@ export function projectMessages(
196
197
 
197
198
  let pendingAssistant: ProviderMessage | null = null;
198
199
  let pendingAssistantMaxSeq = -1;
200
+ // Reasoning block awaiting attachment to the current assistant turn. Only set
201
+ // for REPLAYABLE reasoning (Anthropic signature / redacted-or-Codex encrypted
202
+ // blob) — render-only reasoning is never sent back. Attached as content[0] of
203
+ // the turn's assistant message (Anthropic requires the signed thinking block
204
+ // first on an interleaved-thinking tool-use continuation; a missing/unsigned
205
+ // one is a hard 400). Dropped at any turn/compaction boundary it doesn't reach.
206
+ let pendingReasoning: Extract<ContentBlock, { type: 'reasoning' }> | null = null;
207
+ let pendingReasoningSeq = -1;
199
208
  const flush = (): void => {
200
209
  if (!pendingAssistant) return;
201
210
  const flushed = pendingAssistant;
@@ -237,6 +246,7 @@ export function projectMessages(
237
246
  if (!emittedCompactions.has(compaction)) {
238
247
  emittedCompactions.add(compaction);
239
248
  flush();
249
+ pendingReasoning = null;
240
250
  messages.push({
241
251
  role: 'user',
242
252
  content: [{ type: 'text', text: `[summary of earlier turns]\n${compaction.summary}` }],
@@ -249,6 +259,7 @@ export function projectMessages(
249
259
  switch (e.type) {
250
260
  case 'user_prompt': {
251
261
  flush();
262
+ pendingReasoning = null;
252
263
  // Elided + conversational: collapse to a stub (anchor/tiny kept full).
253
264
  if (conversationalStubbed(e, el)) {
254
265
  messages.push({
@@ -286,9 +297,26 @@ export function projectMessages(
286
297
  recordStable(e.seq);
287
298
  break;
288
299
  }
300
+ case 'reasoning_message': {
301
+ // Render-only reasoning (no signature/encrypted) is never replayed —
302
+ // it exists only for the live/scrollback "Thinking" view. Replayable
303
+ // reasoning is stashed for content[0] of this turn's assistant message.
304
+ if (e.signature || e.encrypted) {
305
+ pendingReasoning = {
306
+ type: 'reasoning',
307
+ text: e.content,
308
+ ...(e.signature ? { signature: e.signature } : {}),
309
+ ...(e.redacted ? { redacted: true } : {}),
310
+ ...(e.encrypted ? { encrypted: e.encrypted } : {}),
311
+ };
312
+ pendingReasoningSeq = e.seq;
313
+ }
314
+ break;
315
+ }
289
316
  case 'assistant_message':
290
317
  flush();
291
318
  if (conversationalStubbed(e, el)) {
319
+ pendingReasoning = null;
292
320
  messages.push({
293
321
  role: 'assistant',
294
322
  content: [{ type: 'text', text: conversationalStub('assistant', e.seq) }],
@@ -303,14 +331,31 @@ export function projectMessages(
303
331
  // tool_use blocks are projected from tool_call_requested events —
304
332
  // which also un-wedges historical logs that already contain one.
305
333
  if (e.content.trim().length === 0) {
334
+ pendingReasoning = null;
306
335
  recordStable(e.seq);
307
336
  break;
308
337
  }
309
- messages.push({ role: 'assistant', content: [{ type: 'text', text: e.content }] });
338
+ {
339
+ const content: Array<ProviderMessage['content'][number]> = [];
340
+ if (pendingReasoning) {
341
+ content.push(pendingReasoning);
342
+ pendingReasoning = null;
343
+ }
344
+ content.push({ type: 'text', text: e.content });
345
+ messages.push({ role: 'assistant', content });
346
+ }
310
347
  recordStable(e.seq);
311
348
  break;
312
349
  case 'tool_call_requested': {
313
- pendingAssistant ??= { role: 'assistant', content: [] };
350
+ if (!pendingAssistant) {
351
+ // Seed the assistant turn so the signed reasoning block is content[0],
352
+ // ahead of every tool_use (Anthropic's interleaved-thinking ordering).
353
+ pendingAssistant = { role: 'assistant', content: pendingReasoning ? [pendingReasoning] : [] };
354
+ if (pendingReasoning) {
355
+ pendingAssistantMaxSeq = Math.max(pendingAssistantMaxSeq, pendingReasoningSeq);
356
+ pendingReasoning = null;
357
+ }
358
+ }
314
359
  pendingAssistantMaxSeq = Math.max(pendingAssistantMaxSeq, e.seq);
315
360
  (pendingAssistant.content as Array<ProviderMessage['content'][number]>).push({
316
361
  type: 'tool_use',
@@ -330,6 +375,10 @@ export function projectMessages(
330
375
  text = toolResultStub(e.callId, toolResultBytes(e.output), recalled);
331
376
  } else if (e.error) {
332
377
  text = `[error:${e.error.kind}] ${e.error.message}`;
378
+ } else if (isToolDisplayResult(e.output)) {
379
+ // Rich result (e.g. a file diff): the model only needs the short
380
+ // `forModel` summary — the structured `display` is for channels.
381
+ text = e.output.forModel;
333
382
  } else {
334
383
  text = typeof e.output === 'string' ? e.output : JSON.stringify(e.output ?? '');
335
384
  }
@@ -361,6 +410,18 @@ export interface StreamResult {
361
410
  readonly error: { readonly message: string; readonly retryable: boolean } | null;
362
411
  /** Token usage reported by the provider on `message_end`, including cache hits/writes. */
363
412
  readonly usage?: TokenUsage;
413
+ /**
414
+ * Reasoning/thinking summary for this provider call, when the model emitted
415
+ * any. The mode emits it as a `reasoning_message` event (so it persists and
416
+ * round-trips). `signature`/`encrypted` carry Anthropic's signed thinking
417
+ * block / redacted blob; `redacted` marks display-suppressed reasoning.
418
+ */
419
+ readonly reasoning?: {
420
+ readonly text: string;
421
+ readonly signature?: string;
422
+ readonly redacted?: boolean;
423
+ readonly encrypted?: string;
424
+ };
364
425
  }
365
426
 
366
427
  /**
@@ -431,12 +492,17 @@ export async function collectProviderStream(
431
492
  // message-derived system text. Prefilling it would duplicate the prompt.
432
493
  // It stays as the side channel `onBeforeProviderCall` hooks use to inject
433
494
  // per-request system text (e.g. the memory consolidation nudge).
495
+ // Forward the per-provider reasoning preference, but only when THIS model
496
+ // advertises `supportsReasoning` — providers ignore the knob otherwise, but
497
+ // gating here keeps requests clean and avoids unsupported-param errors.
498
+ const reqReasoning = descriptor?.supportsReasoning ? ctx.reasoning : undefined;
434
499
  const req = {
435
500
  model: ctx.model,
436
501
  messages: effectiveMessages,
437
502
  ...(toolList ? { tools: toolList } : {}),
438
503
  ...(cacheHints && cacheHints.length > 0 ? { cacheHints } : {}),
439
504
  ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
505
+ ...(reqReasoning ? { reasoning: reqReasoning } : {}),
440
506
  signal: ctx.signal,
441
507
  };
442
508
  const transformed = await ctx.hooks.dispatchBeforeProviderCall(req, {
@@ -453,6 +519,14 @@ export async function collectProviderStream(
453
519
  let stopReason: StopReason = 'end_turn';
454
520
  let error: StreamResult['error'] = null;
455
521
  let usage: TokenUsage | undefined;
522
+ // Reasoning/thinking accumulation for this single provider call. Emitted as a
523
+ // finalized `reasoning_message` by the mode (turn-iterator / goal-loop) so it
524
+ // persists and round-trips; `signature`/`encrypted` carry Anthropic's signed
525
+ // thinking block / redacted blob for replay.
526
+ let reasoningText = '';
527
+ let reasoningSignature: string | undefined;
528
+ let reasoningRedacted = false;
529
+ let reasoningEncrypted: string | undefined;
456
530
 
457
531
  let stream: AsyncIterable<ProviderEvent>;
458
532
  try {
@@ -498,6 +572,25 @@ export async function collectProviderStream(
498
572
  error = { message: event.message, retryable: event.retryable };
499
573
  break;
500
574
  }
575
+ case 'reasoning_delta': {
576
+ reasoningText += event.delta;
577
+ // Live preview only — parallels `assistant_chunk`; renderers
578
+ // accumulate ephemerally and clear on the finalized reasoning_message.
579
+ await ctx.emit({
580
+ type: 'reasoning_chunk',
581
+ sessionId: ctx.sessionId,
582
+ turnId: ctx.turnId,
583
+ source: 'model',
584
+ delta: event.delta,
585
+ });
586
+ break;
587
+ }
588
+ case 'reasoning_signature': {
589
+ if (event.signature) reasoningSignature = event.signature;
590
+ if (event.encrypted) reasoningEncrypted = event.encrypted;
591
+ if (event.redacted) reasoningRedacted = true;
592
+ break;
593
+ }
501
594
  case 'message_start':
502
595
  case 'tool_use_delta':
503
596
  default:
@@ -516,7 +609,25 @@ export async function collectProviderStream(
516
609
  if (!partial.name) continue;
517
610
  finalToolUses.push({ id, name: partial.name, input: partial.input ?? {} });
518
611
  }
519
- return { text, toolUses: finalToolUses, stopReason, error, ...(usage ? { usage } : {}) };
612
+ // Surface reasoning when there's visible text OR an opaque blob to replay
613
+ // (a redacted_thinking block has no text but must still round-trip).
614
+ const reasoning =
615
+ reasoningText.trim().length > 0 || reasoningEncrypted
616
+ ? {
617
+ text: reasoningText,
618
+ ...(reasoningSignature ? { signature: reasoningSignature } : {}),
619
+ ...(reasoningRedacted ? { redacted: true } : {}),
620
+ ...(reasoningEncrypted ? { encrypted: reasoningEncrypted } : {}),
621
+ }
622
+ : undefined;
623
+ return {
624
+ text,
625
+ toolUses: finalToolUses,
626
+ stopReason,
627
+ error,
628
+ ...(usage ? { usage } : {}),
629
+ ...(reasoning ? { reasoning } : {}),
630
+ };
520
631
  }
521
632
 
522
633
  /**
package/src/mode.ts CHANGED
@@ -77,6 +77,13 @@ export interface ModeContext {
77
77
  readonly elision?: ElisionSettings;
78
78
  /** When true, send only always-on + loaded tool schemas; index the rest. */
79
79
  readonly lazyTools?: boolean;
80
+ /**
81
+ * Per-provider reasoning/thinking preference, resolved from the active
82
+ * provider's config at session build. Forwarded to the provider as
83
+ * `ProviderRequest.reasoning` by {@link collectProviderStream}, gated on the
84
+ * active model's `supportsReasoning`. Absent/false → reasoning off.
85
+ */
86
+ readonly reasoning?: { readonly effort?: 'low' | 'medium' | 'high' } | boolean;
80
87
  readonly permissions: PermissionResolver;
81
88
  /**
82
89
  * Optional generic "ask the user a question" gate. Any loop strategy can
package/src/plugin.ts CHANGED
@@ -9,6 +9,7 @@ import type { LifecycleHooks } from './hooks.js';
9
9
  import type { ModeDef } from './mode.js';
10
10
  import type { ProviderDef } from './provider.js';
11
11
  import type { MoxxyRequirement } from './requirements.js';
12
+ import type { SurfaceDef } from './surface.js';
12
13
  import type { ToolDef } from './tool.js';
13
14
  import type { TranscriberDef } from './transcriber.js';
14
15
  import type { SynthesizerDef } from './synthesizer.js';
@@ -16,7 +17,7 @@ import type { ViewRendererDef } from './view-renderer.js';
16
17
  import type { TunnelProviderDef } from './tunnel.js';
17
18
  import type { WorkflowExecutorDef } from './workflow.js';
18
19
 
19
- export type PluginKind = 'tools' | 'provider' | 'mode' | 'compactor' | 'cache-strategy' | 'view-renderer' | 'tunnel-provider' | 'mcp' | 'cli' | 'channel' | 'hooks' | 'agent' | 'command' | 'transcriber' | 'synthesizer' | 'embedder' | 'isolator' | 'workflow-executor';
20
+ export type PluginKind = 'tools' | 'provider' | 'mode' | 'compactor' | 'cache-strategy' | 'view-renderer' | 'tunnel-provider' | 'mcp' | 'cli' | 'channel' | 'surface' | 'hooks' | 'agent' | 'command' | 'transcriber' | 'synthesizer' | 'embedder' | 'isolator' | 'workflow-executor';
20
21
 
21
22
  export interface PluginSpec {
22
23
  readonly name: string;
@@ -44,6 +45,14 @@ export interface PluginSpec {
44
45
  */
45
46
  readonly tunnelProviders?: ReadonlyArray<TunnelProviderDef>;
46
47
  readonly channels?: ReadonlyArray<ChannelDef>;
48
+ /**
49
+ * Interactive surfaces contributed by the plugin — long-lived panes a human
50
+ * and the agent drive together (an embedded terminal, an in-window browser).
51
+ * Registered into the session's `SurfaceRegistry`; the runner exposes them to
52
+ * thin clients over the `surface.*` protocol family. The plugin's own tools
53
+ * reach the same underlying resource through module state. See {@link SurfaceDef}.
54
+ */
55
+ readonly surfaces?: ReadonlyArray<SurfaceDef>;
47
56
  /**
48
57
  * Speech-to-text backends contributed by the plugin. Selected by name via
49
58
  * `session.transcribers.setActive(name)`; channels with audio input use