indusagi-coding-agent 0.1.61 → 0.2.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 (73) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/entry.js +11589 -14719
  3. package/dist/types/boot/contract.d.ts +2 -0
  4. package/dist/types/boot/runners/addon-wiring.d.ts +103 -0
  5. package/dist/types/boot/runners/addon-wiring.test.d.ts +19 -0
  6. package/dist/types/boot/runners/checkpoint.d.ts +133 -0
  7. package/dist/types/boot/runners/checkpoint.test.d.ts +12 -0
  8. package/dist/types/boot/runners/delegate-runner.d.ts +83 -0
  9. package/dist/types/boot/runners/delegate-runner.test.d.ts +13 -0
  10. package/dist/types/boot/runners/memdir.d.ts +103 -0
  11. package/dist/types/boot/runners/memdir.test.d.ts +12 -0
  12. package/dist/types/boot/runners/read-state.d.ts +82 -0
  13. package/dist/types/boot/runners/read-state.test.d.ts +10 -0
  14. package/dist/types/boot/runners/session.d.ts +37 -2
  15. package/dist/types/boot/runners/session.test.d.ts +10 -0
  16. package/dist/types/briefing/context-docs.d.ts +38 -0
  17. package/dist/types/briefing/context-docs.test.d.ts +18 -0
  18. package/dist/types/briefing/index.d.ts +2 -0
  19. package/dist/types/capability-deck/cards/index.d.ts +6 -0
  20. package/dist/types/capability-deck/cards/memory-card.d.ts +9 -10
  21. package/dist/types/capability-deck/cards/plan-file.d.ts +56 -0
  22. package/dist/types/capability-deck/cards/plan-tools.d.ts +97 -0
  23. package/dist/types/capability-deck/cards/plan-tools.test.d.ts +9 -0
  24. package/dist/types/capability-deck/checkpoint.int.test.d.ts +25 -0
  25. package/dist/types/capability-deck/index.d.ts +1 -1
  26. package/dist/types/capability-deck/read-edit-gate.int.test.d.ts +21 -0
  27. package/dist/types/conductor/bash-guard.d.ts +106 -0
  28. package/dist/types/conductor/bash-guard.test.d.ts +17 -0
  29. package/dist/types/conductor/conductor.d.ts +38 -6
  30. package/dist/types/conductor/contract.d.ts +221 -2
  31. package/dist/types/conductor/diagnostics.d.ts +183 -0
  32. package/dist/types/conductor/diagnostics.test.d.ts +10 -0
  33. package/dist/types/conductor/index.d.ts +4 -1
  34. package/dist/types/conductor/permission-gate.integration.test.d.ts +22 -0
  35. package/dist/types/conductor/permission-wiring.test.d.ts +14 -0
  36. package/dist/types/conductor/permissions.d.ts +217 -0
  37. package/dist/types/conductor/permissions.test.d.ts +12 -0
  38. package/dist/types/conductor/plan-mode.integration.test.d.ts +23 -0
  39. package/dist/types/conductor/post-edit-diagnostics.test.d.ts +13 -0
  40. package/dist/types/conductor/transcript-store/serialize.test.d.ts +10 -0
  41. package/dist/types/conductor/transcript-store/store.d.ts +18 -0
  42. package/dist/types/console/components/Banner.d.ts +28 -6
  43. package/dist/types/console/components/Emblem.d.ts +49 -0
  44. package/dist/types/console/components/StatusBar.d.ts +14 -3
  45. package/dist/types/console/components/WorkingIndicator.d.ts +44 -0
  46. package/dist/types/console/components/WorkingIndicator.test.d.ts +9 -0
  47. package/dist/types/console/components/banner-sweep.d.ts +55 -0
  48. package/dist/types/console/components/banner.test.d.ts +9 -0
  49. package/dist/types/console/contract.d.ts +41 -7
  50. package/dist/types/console/input/keymap.d.ts +10 -1
  51. package/dist/types/console/overlays/approval-queue.d.ts +71 -0
  52. package/dist/types/console/overlays/approval.d.ts +104 -0
  53. package/dist/types/console/overlays/approval.test.d.ts +17 -0
  54. package/dist/types/console/overlays/host.d.ts +4 -3
  55. package/dist/types/console/overlays/index.d.ts +2 -0
  56. package/dist/types/console/theme/adapter.d.ts +19 -0
  57. package/dist/types/console/theme/index.d.ts +1 -1
  58. package/dist/types/console/theme/palette.d.ts +25 -0
  59. package/dist/types/console/theme/tokens.d.ts +23 -1
  60. package/dist/types/index.d.ts +1 -1
  61. package/dist/types/launch/contract.d.ts +2 -0
  62. package/dist/types/launch/index.d.ts +1 -1
  63. package/dist/types/launch/oauth.d.ts +13 -0
  64. package/dist/types/settings/contract.d.ts +59 -0
  65. package/dist/types/settings/index.d.ts +2 -2
  66. package/dist/types/window-budget/condenser.d.ts +15 -1
  67. package/dist/types/window-budget/index.d.ts +3 -1
  68. package/dist/types/window-budget/microcompact.d.ts +68 -0
  69. package/dist/types/window-budget/microcompact.test.d.ts +16 -0
  70. package/dist/types/window-budget/rehydrate.d.ts +56 -0
  71. package/dist/types/workspace/brand.d.ts +6 -0
  72. package/dist/types/workspace/index.d.ts +1 -1
  73. package/package.json +2 -2
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Permission rule engine + the `canUseTool` gate seam.
3
+ *
4
+ * This module is the product-side brain of the permission stack. It is split in
5
+ * two layers:
6
+ *
7
+ * 1. **A pure rule engine** — {@link resolveRuleDecision} maps a single tool
8
+ * call `(toolName, input, mode, rules)` to a {@link PermissionBehavior}
9
+ * (`allow` / `ask` / `deny`). It is total, synchronous, and side-effect free
10
+ * so the precedence (deny > ask > mode auto-allow > allow) is exhaustively
11
+ * unit-testable. Rule strings are induscode-style: a bare tool name
12
+ * (`"Bash"`) or a tool name with an argument specifier
13
+ * (`"Bash(npm run test:*)"`).
14
+ *
15
+ * 2. **A gate factory** — {@link createPermissionGate} turns that engine into a
16
+ * framework {@link CanUseToolFn}: the hard async hook the framework agent
17
+ * awaits immediately before every tool runs. An `ask` decision is routed to
18
+ * an OPTIONAL host-supplied approval resolver; when the resolver is absent
19
+ * (a non-interactive boot) `ask` denies with a clear message. An
20
+ * `allow-always` approval appends a session-scoped allow rule so later
21
+ * identical calls auto-allow.
22
+ *
23
+ * Backwards-compatibility: the framework `canUseTool` hook is itself optional, so
24
+ * a session that never builds a gate keeps today's allow-all behavior. A gate
25
+ * built from an empty rule set in `default` mode allows read-only tools and asks
26
+ * for everything else — and, with no resolver, the `ask` denials are explicit
27
+ * rather than silent.
28
+ */
29
+ import type { PermissionMode } from "../settings";
30
+ /** Re-exported so consumers of the engine get the mode vocabulary in one import. */
31
+ export type { PermissionMode };
32
+ /** The verdict the rule engine renders for a single tool call. */
33
+ export type PermissionBehavior = "allow" | "ask" | "deny";
34
+ /**
35
+ * One parsed permission rule: a behavior plus the tool it targets, with an
36
+ * optional argument specifier (`ruleContent`) that further narrows the match.
37
+ */
38
+ export interface PermissionRule {
39
+ /** What happens when this rule matches a tool call. */
40
+ readonly ruleBehavior: PermissionBehavior;
41
+ /** The tool (and optional argument specifier) this rule selects. */
42
+ readonly ruleValue: {
43
+ /** The canonical tool name, e.g. `"Bash"` / `"bash"` (matched case-insensitively). */
44
+ readonly toolName: string;
45
+ /** An optional argument specifier, e.g. `"npm run test:*"`. Absent = bare match. */
46
+ readonly ruleContent?: string;
47
+ };
48
+ }
49
+ /**
50
+ * The framework's tool-permission verdict. Mirrors `indusagi/agent`'s
51
+ * `PermissionDecision`; re-declared here so the product engine never imports the
52
+ * framework just for a structural type (the gate is assignable to the
53
+ * framework's `CanUseToolFn` because the shapes coincide).
54
+ */
55
+ export type PermissionDecision = {
56
+ behavior: "allow";
57
+ updatedInput?: unknown;
58
+ } | {
59
+ behavior: "deny";
60
+ message: string;
61
+ };
62
+ /**
63
+ * The hard permission gate the framework agent awaits before each tool executes.
64
+ * Structurally identical to `indusagi/agent`'s `CanUseToolFn`, so a value of this
65
+ * type is accepted by `new Agent({ canUseTool })` with no cast.
66
+ */
67
+ export type CanUseToolFn = (toolName: string, input: unknown, opts: {
68
+ signal?: AbortSignal;
69
+ }) => Promise<PermissionDecision>;
70
+ /**
71
+ * The outcome of the host approval prompt raised for an `ask` decision:
72
+ * - `allow-once` — run this single call, do not remember.
73
+ * - `allow-always` — run it and append a session allow rule for the tool.
74
+ * - `deny` — block this call.
75
+ */
76
+ export type ApprovalChoice = "allow-once" | "allow-always" | "deny";
77
+ /**
78
+ * The OPTIONAL host approval resolver. When present, an `ask` decision awaits it;
79
+ * the host (an interactive overlay) returns the user's choice. It MUST resolve to
80
+ * `"deny"` on abort so a cancelled turn never hangs on a pending prompt. When the
81
+ * resolver is absent (non-interactive boot / oneshot / link), an `ask` decision
82
+ * deterministically denies.
83
+ */
84
+ export type ApprovalResolver = (toolName: string, input: unknown, opts: {
85
+ signal?: AbortSignal;
86
+ }) => Promise<ApprovalChoice>;
87
+ /**
88
+ * Tool names known to only inspect state (never mutate). Mirrors the framework's
89
+ * `READ_ONLY_TOOL_NAMES`; matched case-insensitively. A tool the framework marks
90
+ * `readOnly: true` is also auto-allowed via {@link createPermissionGate}'s
91
+ * `readOnlyToolNames` option, so this set is the static fallback for callers that
92
+ * only know names.
93
+ */
94
+ export declare const READ_ONLY_TOOL_NAMES: ReadonlySet<string>;
95
+ /**
96
+ * Tool names that mutate the workspace by editing or writing files. Auto-allowed
97
+ * under `acceptEdits`, and (together with every other non-read-only tool) denied
98
+ * under `plan`.
99
+ */
100
+ export declare const EDIT_TOOL_NAMES: ReadonlySet<string>;
101
+ /** Whether `toolName` is a statically-known read-only tool. */
102
+ export declare function isReadOnlyToolName(toolName: string, extra?: ReadonlySet<string>): boolean;
103
+ /** Whether `toolName` is an edit/write tool (auto-allowed under `acceptEdits`). */
104
+ export declare function isEditToolName(toolName: string): boolean;
105
+ /**
106
+ * Parse an induscode-style rule string into its tool name and optional argument
107
+ * specifier.
108
+ *
109
+ * - `"Bash"` → `{ toolName: "Bash" }`
110
+ * - `"Bash(npm run test:*)"` → `{ toolName: "Bash", ruleContent: "npm run test:*" }`
111
+ * - `"mcp__server"` → `{ toolName: "mcp__server" }`
112
+ *
113
+ * Whitespace around the tool name is trimmed; an empty argument specifier
114
+ * (`"Bash()"`) is treated as a bare rule.
115
+ */
116
+ export declare function parseRule(raw: string): {
117
+ toolName: string;
118
+ ruleContent?: string;
119
+ };
120
+ /**
121
+ * Build a {@link PermissionRule} from a behavior and an induscode-style rule
122
+ * string. The single place a raw settings list entry becomes a typed rule.
123
+ */
124
+ export declare function makeRule(ruleBehavior: PermissionBehavior, raw: string): PermissionRule;
125
+ /**
126
+ * Whether a tool call matches a rule (the ANY-match semantics used for deny/ask).
127
+ *
128
+ * The tool name must match (case-insensitive), with an `mcp__server` rule also
129
+ * matching every `mcp__server__tool` under it (the induscode MCP-wildcard
130
+ * convention). A bare rule (no specifier) matches any arguments.
131
+ *
132
+ * When the rule carries an argument specifier:
133
+ * - for the **shell** tool the command is split into its constituent
134
+ * sub-commands and the specifier is tested against EACH — a match on ANY
135
+ * sub-command counts (so `Bash(rm:*)` denies `git status && rm x`);
136
+ * - for any other tool the specifier is tested against the single rendered
137
+ * subject (the command field, or a JSON encoding).
138
+ *
139
+ * The any-match semantics are correct for `deny` and `ask` (a single offending
140
+ * sub-command should trigger them). The `allow` branch needs ALL sub-commands
141
+ * covered instead — see {@link bashAllowCoversAll}.
142
+ */
143
+ export declare function toolMatchesRule(toolName: string, input: unknown, rule: PermissionRule): boolean;
144
+ /**
145
+ * Resolve the behavior for a single tool call against the rule set and mode.
146
+ *
147
+ * Precedence, highest first:
148
+ * 1. **deny rules** — any matching `deny` rule blocks, regardless of mode. Deny
149
+ * wins across every tier (the caller concatenates the tiers before passing
150
+ * the list in, and this scans all deny rules first).
151
+ * 2. **plan mode** — denies any mutating (non-read-only) tool outright.
152
+ * 3. **ask rules** — a matching `ask` rule forces a prompt (unless a deny
153
+ * already fired). Mode auto-allow does NOT override an explicit ask.
154
+ * 4. **bypass mode** — allows everything not already denied/asked.
155
+ * 5. **read-only auto-allow** — a known read-only tool allows in any mode.
156
+ * 6. **acceptEdits mode** — auto-allows edit/write tools.
157
+ * 7. **allow rules** — a matching `allow` rule (or, for the shell tool, the
158
+ * allow rule SET) allows.
159
+ * 8. **fallthrough** — `ask` (the safe default: prompt, then deny when no
160
+ * resolver is wired).
161
+ *
162
+ * The catastrophic-command blocklist sits ABOVE all of this (step 0): a shell
163
+ * command on the built-in blocklist (`rm -rf /`, fork bombs, `curl | sh`, …) is
164
+ * denied regardless of rules or mode — even `bypass` cannot run it. See
165
+ * {@link evaluateCatastrophic}.
166
+ *
167
+ * Shell handling threads through the per-sub-command matching: a compound
168
+ * command (`a && b; c | d`) is split into its constituent commands and each rule
169
+ * is evaluated against each — a deny/ask on ANY sub-command fires, while
170
+ * auto-allow requires the allow rule set to cover EVERY sub-command.
171
+ *
172
+ * Pure and total: no I/O, no async, deterministic for a given input.
173
+ */
174
+ export declare function resolveRuleDecision(toolName: string, input: unknown, rules: readonly PermissionRule[], mode: PermissionMode, readOnlyExtra?: ReadonlySet<string>): PermissionBehavior;
175
+ /** Configuration for {@link createPermissionGate}. */
176
+ export interface PermissionGateConfig {
177
+ /**
178
+ * The ordered rule set, already concatenated across the project/global tiers.
179
+ * Deny rules are scanned first so a deny anywhere in the list wins.
180
+ */
181
+ readonly rules: readonly PermissionRule[];
182
+ /** A live getter for the current permission mode (so a mode switch is honored). */
183
+ readonly mode: () => PermissionMode;
184
+ /**
185
+ * The OPTIONAL host approval resolver consulted on an `ask` decision. Absent on
186
+ * a non-interactive boot — then `ask` deterministically denies.
187
+ */
188
+ readonly requestApproval?: ApprovalResolver;
189
+ /**
190
+ * Append a session-scoped allow rule when the host returns `allow-always`. The
191
+ * conductor owns the mutable rule list and passes a setter here so the new rule
192
+ * is visible to subsequent calls within the session.
193
+ */
194
+ readonly appendAllowRule?: (rule: PermissionRule) => void;
195
+ /**
196
+ * Tool names (beyond {@link READ_ONLY_TOOL_NAMES}) to treat as read-only —
197
+ * derived from the deck's `readOnly: true` tool flags so MCP/custom read-only
198
+ * tools also auto-allow.
199
+ */
200
+ readonly readOnlyToolNames?: ReadonlySet<string>;
201
+ }
202
+ /**
203
+ * Build a framework {@link CanUseToolFn} from the rule engine + the current mode.
204
+ *
205
+ * The returned gate:
206
+ * - resolves the behavior via {@link resolveRuleDecision},
207
+ * - on `allow` proceeds (the framework keeps the validated args),
208
+ * - on `deny` short-circuits with a clear message (the framework turns it into
209
+ * an `isError` tool result so the model sees why),
210
+ * - on `ask` awaits {@link PermissionGateConfig.requestApproval} when present —
211
+ * `allow-once` proceeds, `allow-always` proceeds and appends a session allow
212
+ * rule, `deny` blocks — and DENIES when no resolver is wired (the safe
213
+ * non-interactive default).
214
+ *
215
+ * The result is assignable to `indusagi/agent`'s `CanUseToolFn`.
216
+ */
217
+ export declare function createPermissionGate(config: PermissionGateConfig): CanUseToolFn;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Permission rule engine + gate — unit tests.
3
+ *
4
+ * Exhaustively pins the pure decision ({@link resolveRuleDecision}) precedence
5
+ * (deny > plan-deny > ask > bypass > read-only > acceptEdits > allow > ask
6
+ * fallthrough), the induscode rule parsing/matching ({@link parseRule},
7
+ * {@link toolMatchesRule}, including argument specifiers and the `mcp__` wildcard),
8
+ * the mode behaviors, and the {@link createPermissionGate} `ask` routing — a host
9
+ * resolver returns allow-once / allow-always (which appends a session allow rule)
10
+ * / deny, and an ABSENT resolver denies. No framework, no network.
11
+ */
12
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Plan-mode round-trip — integration across the gate + the conductor handshake.
3
+ *
4
+ * Two proofs, against the REAL wired pieces (the rule engine's gate + the live
5
+ * conductor), not mocks of an unwired module:
6
+ *
7
+ * 1. **The gate denies edits in plan mode.** A `createPermissionGate` whose mode
8
+ * getter reads the conductor's LIVE `permissionMode()` allows an edit in
9
+ * `default` mode, denies it once the conductor enters plan mode
10
+ * (`togglePlanMode(true)` / `setPermissionMode('plan')`), and allows it again
11
+ * once an approved exit restores the prior mode. This is the exact gate the
12
+ * conductor threads into the framework `Agent`, so the deny lands where tools
13
+ * execute.
14
+ *
15
+ * 2. **ExitPlanMode approval restores the mode, persists the plan, and resumes.**
16
+ * A scripted fake agent emits a `tool_execution_end` carrying the
17
+ * `{ exitPlan: true, plan }` detail the conductor intercepts. With an approval
18
+ * resolver installed that approves, a turn run while in plan mode: restores
19
+ * the captured pre-plan mode, writes the plan to a file under `plansDir`, and
20
+ * enqueues the approved plan as a follow-up so the model proceeds from it. A
21
+ * rejecting resolver keeps the session in plan mode and writes nothing.
22
+ */
23
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Conductor-integration tests for the post-edit live-diagnostics seam.
3
+ *
4
+ * Drives a scripted {@link AgentLike} that emits a `tool_execution_start` for an
5
+ * `edit`/`write` tool during `prompt`, with an injected {@link DiagnosticsEngine}
6
+ * over a scripted in-memory runner (no real tsc spawn). Verifies:
7
+ * - a turn that introduces a NEW diagnostic enqueues the summary follow-up;
8
+ * - a clean edit (no new diagnostics) enqueues nothing;
9
+ * - re-editing a file re-surfaces a previously-cleared diagnostic;
10
+ * - a non-TS edit never triggers the checker;
11
+ * - the injected follow-up turn (which edits nothing) does not loop.
12
+ */
13
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Transcript serialization — head usage round-trip.
3
+ *
4
+ * Focused coverage for the cost-cmd durability seam: the head line now carries
5
+ * the session's cumulative {@link Usage}, and {@link encodeHead} →
6
+ * {@link parseSessionText} must round-trip it faithfully so resume can restore the
7
+ * running token/cost total instead of seeding zero. Heads without usage (legacy
8
+ * files, fresh sessions) must still parse cleanly with `usage` absent.
9
+ */
10
+ export {};
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import type { AgentMessage, SessionHead, TranscriptEntry, TranscriptRole } from "../contract";
29
29
  import { TRANSCRIPT_SCHEMA } from "../contract";
30
+ import type { Usage } from "indusagi/ai";
30
31
  /**
31
32
  * Where a transcript's NDJSON log lives. The store appends lines and, on branch,
32
33
  * rewrites the whole file; a backend supplies those three primitives plus a way
@@ -101,6 +102,13 @@ export declare class TranscriptStore {
101
102
  get sessionId(): string;
102
103
  /** The current head (session id + active leaf). */
103
104
  get head(): SessionHead;
105
+ /**
106
+ * The cumulative session usage last persisted onto the head, or `undefined`
107
+ * when none has been written (a fresh session, or a legacy file without a
108
+ * usage-bearing head). `resume` reads this to restore the running token/cost
109
+ * total instead of seeding zero.
110
+ */
111
+ get usage(): Usage | undefined;
104
112
  /** Number of nodes currently in the transcript. */
105
113
  get size(): number;
106
114
  /** Backend location this session persists to (for diagnostics/UI). */
@@ -116,6 +124,16 @@ export declare class TranscriptStore {
116
124
  * a settled turn.
117
125
  */
118
126
  append(content: AgentMessage, role?: TranscriptRole, meta?: Readonly<Record<string, unknown>>): Promise<TranscriptEntry>;
127
+ /**
128
+ * Persist the session's cumulative usage onto the head and rewrite the backing
129
+ * file so the running token/cost total survives a reload. Distinct from
130
+ * {@link append}, which only appends an entry line and never rewrites the head:
131
+ * usage durability MUST flow through here so the re-emitted head line carries
132
+ * the latest tally. Leaves the node tree and active leaf untouched.
133
+ *
134
+ * @param usage the cumulative usage to pin on the head
135
+ */
136
+ persistUsage(usage: Usage): Promise<void>;
119
137
  /**
120
138
  * Repoint the head at an earlier node, forking a branch. The next {@link append}
121
139
  * becomes a child of `id`; existing nodes are untouched. Rewrites the head line.
@@ -33,6 +33,26 @@ export interface BannerProps {
33
33
  * single compact header line. Wired from the verbose / quiet-startup flag.
34
34
  */
35
35
  readonly quiet?: boolean;
36
+ /**
37
+ * When set, the masthead auto-condenses to a single emblem + brand + model
38
+ * line (the repeat-launch presentation): the user has already seen this
39
+ * version's full masthead, so the big wordmark is skipped. Distinct from
40
+ * {@link quiet}, which is the manual suppression toggle; either collapses the
41
+ * banner, but `compact` keeps the small emblem and the welcome line.
42
+ */
43
+ readonly compact?: boolean;
44
+ /**
45
+ * The signed-in account label, used for the personalized "Welcome back,
46
+ * {name}!" line. Absent → the plain "Welcome back!" fallback is shown.
47
+ */
48
+ readonly name?: string;
49
+ /**
50
+ * Opt-in static colour-sweep flourish: when set, the wordmark and emblem fill
51
+ * are tinted along a frozen primary→secondary gradient instead of the flat
52
+ * accent. The caller is responsible for suppressing it under reduced-motion /
53
+ * non-TTY; this prop is simply the resolved on/off decision.
54
+ */
55
+ readonly sweep?: boolean;
36
56
  /** The gathered session resources rendered as the Startup Map panel. */
37
57
  readonly startup?: StartupMap;
38
58
  /** Out-of-band lines drawn above the wordmark (errors, warnings, info). */
@@ -43,12 +63,14 @@ export interface BannerProps {
43
63
  /**
44
64
  * Render the console masthead.
45
65
  *
46
- * In the default (loud) mode this is the block-letter wordmark, the brand /
47
- * version line, the optional notices region, the bordered Startup Map, and the
48
- * changelog block. The {@link BannerProps.quiet} flag collapses all of that to a
49
- * single compact header line (brand + version + model), still carrying the
50
- * notices and a condensed changelog so nothing important is silently dropped.
66
+ * In the default (loud) mode this is the two-tone emblem beside the block-letter
67
+ * wordmark, the brand / version line, the personalized welcome line, the
68
+ * optional notices region, the bordered Startup Map, and the changelog block.
69
+ * The {@link BannerProps.quiet} or {@link BannerProps.compact} flags collapse
70
+ * all of that to a single compact header line (emblem glyph + brand + version +
71
+ * model), still carrying the welcome line, the notices, and a condensed
72
+ * changelog so nothing important is silently dropped.
51
73
  *
52
74
  * @param props the wordmark context, version, session facts, and startup chrome
53
75
  */
54
- export declare function Banner({ theme, modelId, workspace, version, verbose, quiet, startup, notices, changelog, }: BannerProps): JSX.Element;
76
+ export declare function Banner({ theme, modelId, workspace, version, verbose, quiet, compact, name, sweep, startup, notices, changelog, }: BannerProps): JSX.Element;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Emblem — the two-tone block monogram that sits to the left of the wordmark
3
+ * (roadmap #2).
4
+ *
5
+ * Before this, the masthead was lettering alone — the "INDUS CODE" wordmark
6
+ * *was* the whole identity, painted in one flat accent. The emblem gives the
7
+ * brand a *mark*: a small fixed-width block glyph built from the box quadrant
8
+ * family (`▛ ▜ ▙ ▟ ▝ ▘ ▗ ▖ █ ▓`), split per row into a bright "fill" span and a
9
+ * dim "shadow" span so it reads as a single solid shape lit from one corner.
10
+ * The fill is the primary accent; the shadow is the secondary accent — the two
11
+ * distinct palette hues the scheme already carries — so the mark is genuinely
12
+ * two-tone rather than a single colour with a darker edge.
13
+ *
14
+ * It is purely presentational: a fixed glyph grid tinted through the framework
15
+ * {@link InkThemeAdapter} at render time, no state, no effects, no props beyond
16
+ * the theme and an optional row-colour override the banner uses to fold the
17
+ * emblem's fill into a frozen colour-sweep.
18
+ *
19
+ * Layout is a `flexDirection="column"` of one `<Box>` per row; within a row the
20
+ * fill and shadow are adjacent `<Text>` spans so each row's two tones share a
21
+ * line. The whole emblem is the same display width on every row, so it stacks
22
+ * into a clean column the wordmark can sit beside in a parent row Box.
23
+ */
24
+ import type { InkThemeAdapter } from "indusagi/react-ink";
25
+ /** How many terminal rows the emblem occupies (its glyph-grid height). */
26
+ export declare const EMBLEM_HEIGHT: number;
27
+ /** What the {@link Emblem} renders. */
28
+ export interface EmblemProps {
29
+ /** The framework adapter that turns token roles into terminal colours. */
30
+ readonly theme: InkThemeAdapter;
31
+ /**
32
+ * Optional per-row fill colour (a `#rrggbb` hex), indexed by row. When supplied
33
+ * (the banner's frozen colour-sweep), row `i`'s fill is painted with
34
+ * `rowColors[i]` via chalk's hex path instead of the flat accent role; the
35
+ * shadow keeps the secondary accent so the mark stays two-tone. Absent rows
36
+ * fall back to the accent role.
37
+ */
38
+ readonly rowColors?: readonly string[];
39
+ }
40
+ /**
41
+ * Render the two-tone block emblem.
42
+ *
43
+ * Each row paints its fill span in the accent role (or the supplied sweep colour
44
+ * for that row) and its shadow span in the secondary `customMessage` role, so the
45
+ * monogram reads as one solid mark with a lit and a shadowed face.
46
+ *
47
+ * @param props the theme adapter and an optional per-row fill-colour override
48
+ */
49
+ export declare function Emblem({ theme, rowColors }: EmblemProps): JSX.Element;
@@ -10,6 +10,7 @@
10
10
  * (Renamed surface: the console's status strip.)
11
11
  */
12
12
  import { type InkThemeAdapter, type SessionSnapshot, type StatusMessage } from "indusagi/react-ink";
13
+ import type { PermissionMode } from "../../settings";
13
14
  /** What the {@link StatusBar} renders. */
14
15
  export interface StatusBarProps {
15
16
  /** The framework adapter that turns token roles into terminal colours. */
@@ -22,10 +23,20 @@ export interface StatusBarProps {
22
23
  readonly providerCount: number;
23
24
  /** The transient status toast, when one is showing. */
24
25
  readonly status?: StatusMessage;
26
+ /**
27
+ * The active permission mode. When supplied, a small badge is rendered above the
28
+ * footer (parallel to the transient status toast) so the user can see at a glance
29
+ * which of the four cycled modes is live: `default` (tools prompt as usual),
30
+ * `acceptEdits` (edits auto-approved), `plan` (mutating tools blocked), or
31
+ * `bypass` (every tool auto-allowed — drawn in an alert colour). The
32
+ * `bypassPermissions` alias renders as `bypass`. Omitted renders nothing extra.
33
+ */
34
+ readonly permissionMode?: PermissionMode;
25
35
  }
26
36
  /**
27
- * Render the toast row above the footer strip.
37
+ * Render the toast row above the footer strip, plus an optional permission-mode
38
+ * badge when a non-default mode is active.
28
39
  *
29
- * @param props the snapshot + status to display
40
+ * @param props the snapshot + status (+ optional permission mode) to display
30
41
  */
31
- export declare function StatusBar({ theme, snapshot, branch, providerCount, status, }: StatusBarProps): JSX.Element;
42
+ export declare function StatusBar({ theme, snapshot, branch, providerCount, status, permissionMode, }: StatusBarProps): JSX.Element;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WorkingIndicator — the live "agent is working" affordance.
3
+ *
4
+ * While a turn is in flight (`busy`) this draws an animated braille spinner, a
5
+ * label, an elapsed-seconds clock, and an "esc to interrupt" hint. The label is
6
+ * a rotating whimsical word (changing every few seconds, induscode-style) for
7
+ * the general thinking/streaming case, and a concrete phase label while the
8
+ * agent runs tools or compacts context.
9
+ *
10
+ * It renders nothing when idle, and self-drives its animation via a short
11
+ * interval that ticks ONLY while busy (torn down the moment `busy` clears), so an
12
+ * idle console does no spinner work. It is a sibling row above the
13
+ * {@link Composer} and does NOT consume the sticky status toast — so a previous
14
+ * toast (e.g. the permission-mode line) can never mask that the agent is working.
15
+ */
16
+ import type { InkThemeAdapter } from "indusagi/react-ink";
17
+ /**
18
+ * The rotating "thinking" words shown while the agent reasons/streams. Whimsical
19
+ * present participles, in the spirit of induscode's spinner — purely cosmetic.
20
+ */
21
+ export declare const WORKING_WORDS: readonly ["Cogitating", "Pondering", "Noodling", "Simmering", "Brewing", "Percolating", "Conjuring", "Tinkering", "Wrangling", "Synthesizing", "Computing", "Musing", "Scheming", "Hatching", "Whirring", "Crunching", "Spelunking", "Untangling", "Marinating", "Ruminating", "Architecting", "Finagling", "Concocting", "Deliberating", "Orchestrating", "Calibrating", "Channeling", "Vibing", "Forging", "Assembling"];
22
+ /** Pick a working word by (wrapping, non-negative) index. Exported for testing. */
23
+ export declare function pickWord(index: number): string;
24
+ /** What the {@link WorkingIndicator} renders. */
25
+ export interface WorkingIndicatorProps {
26
+ /** The framework adapter that turns token roles into terminal colours. */
27
+ readonly theme: InkThemeAdapter;
28
+ /** Whether a turn is in flight. When false the indicator renders nothing. */
29
+ readonly busy: boolean;
30
+ /** The conductor snapshot phase, used to pick a concrete label when relevant. */
31
+ readonly phase?: string;
32
+ }
33
+ /**
34
+ * The concrete label for the phases where a specific word is more useful than a
35
+ * whimsical one. Returns null for the general thinking/streaming case (the caller
36
+ * then shows a rotating {@link WORKING_WORDS} word instead). Exported for testing.
37
+ */
38
+ export declare function phaseLabel(phase: string | undefined): string | null;
39
+ /**
40
+ * Render the animated working row, or nothing when idle.
41
+ *
42
+ * @param props the theme, the busy flag, and the snapshot phase
43
+ */
44
+ export declare function WorkingIndicator({ theme, busy, phase, }: WorkingIndicatorProps): JSX.Element | null;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * WorkingIndicator — unit tests for the pure label/word logic.
3
+ *
4
+ * The animated render is driven by an Ink-only timer that is impractical to
5
+ * assert headlessly; the load-bearing pure logic is {@link phaseLabel} (concrete
6
+ * labels for tool/compaction phases) and {@link pickWord} (the rotating whimsical
7
+ * word for the general thinking case).
8
+ */
9
+ export {};
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Banner colour-sweep helpers — the pure, render-agnostic maths behind the
3
+ * optional startup flourish (roadmap #14) and the personalized welcome line
4
+ * (#7).
5
+ *
6
+ * Two concerns, no React or Ink, no I/O, no state:
7
+ *
8
+ * - {@link rowGradient} computes a *frozen* (single-pass, no animation timer)
9
+ * colour for one wordmark row by lerping between two palette hexes across
10
+ * the block's rows. The banner paints row `i` of `n` with this colour when
11
+ * the opt-in sweep is on; off, the wordmark stays a flat accent. Because it
12
+ * takes no clock and never schedules a frame, it is reduced-motion safe by
13
+ * construction — the suppression decision lives at the call site, which
14
+ * simply does not ask for a gradient when motion is reduced.
15
+ *
16
+ * - {@link welcomeLine} formats the "Welcome back{, name}!" greeting from the
17
+ * signed-in account label, with a length guard and a name-less fallback, so
18
+ * the banner and its tests share one formatter rather than re-deriving the
19
+ * string.
20
+ *
21
+ * Everything here is referentially transparent: same inputs → same output.
22
+ */
23
+ /**
24
+ * Format the personalized welcome greeting.
25
+ *
26
+ * Returns `"Welcome back, {name}!"` for a usable name, and the plain
27
+ * `"Welcome back!"` fallback when the name is absent, blank, or too long to read
28
+ * as a name. The name is trimmed of surrounding whitespace before the checks so
29
+ * a padded label does not slip past the guards.
30
+ *
31
+ * @param name the signed-in account label, if one is known
32
+ * @returns the greeting string (never the tag markup — plain text)
33
+ */
34
+ export declare function welcomeLine(name?: string): string;
35
+ /**
36
+ * The frozen sweep colour for one wordmark row.
37
+ *
38
+ * Lerps from `primaryHex` (the first row) to `secondaryHex` (the last row)
39
+ * across the `rowCount` rows of the block, returning the colour for row
40
+ * `rowIndex` as a `#rrggbb` hex string. A single-row block pins to the primary;
41
+ * the endpoints are exact (row 0 = primary, row n-1 = secondary). If either hex
42
+ * cannot be parsed, the corresponding raw input hex is returned so the banner
43
+ * still gets a usable colour string rather than nothing.
44
+ *
45
+ * This computes a *static* gradient — there is no timer, no frame loop, no
46
+ * clock. The whole sweep is laid down in the one render pass, which is exactly
47
+ * why it is safe under reduced-motion: the flourish is colour, not movement.
48
+ *
49
+ * @param rowIndex the zero-based row being painted
50
+ * @param rowCount the total number of rows in the wordmark block
51
+ * @param primaryHex the colour of the first row (e.g. the accent)
52
+ * @param secondaryHex the colour of the last row (e.g. the secondary accent)
53
+ * @returns a `#rrggbb` hex string (or a raw input hex when one is unparseable)
54
+ */
55
+ export declare function rowGradient(rowIndex: number, rowCount: number, primaryHex: string, secondaryHex: string): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Banner colour-sweep helpers — pure behavioural tests.
3
+ *
4
+ * These exercise the render-agnostic maths behind the masthead flourish without
5
+ * mounting Ink: the welcome-line formatter (#7) and the frozen colour-sweep
6
+ * gradient (#14). Both are referentially transparent, so the assertions are
7
+ * exact.
8
+ */
9
+ export {};
@@ -56,13 +56,17 @@ export type { SessionSnapshot, ToolExecutionState, StatusMessage, UiDisplayBlock
56
56
  /** Re-exported conductor vocabulary the console renders and drives. */
57
57
  export type { SessionConductor, SessionSignal, ConductorState };
58
58
  /**
59
- * The two built-in colour schemes the console ships with.
60
- *
61
- * Named for the time-of-day they evoke rather than a bare light/dark axis:
62
- * - `midnight` — a low-luminance scheme for dark terminals.
63
- * - `daylight` — a high-luminance scheme for light terminals.
59
+ * The built-in colour schemes the console ships with.
60
+ *
61
+ * Named for the time-of-day they evoke rather than a bare light/dark axis, with
62
+ * a daltonized (color-blind-friendly) variant of each:
63
+ * - `midnight` — a low-luminance scheme for dark terminals.
64
+ * - `daylight` — a high-luminance scheme for light terminals.
65
+ * - `midnight-cb` — the dark scheme re-derived so success vs failure separates
66
+ * off the red-green axis (success → blue), deuteran/protan-safe.
67
+ * - `daylight-cb` — the light scheme's color-blind-safe counterpart.
64
68
  */
65
- export type ThemeScheme = "midnight" | "daylight";
69
+ export type ThemeScheme = "midnight" | "daylight" | "midnight-cb" | "daylight-cb";
66
70
  /** The default scheme applied before any user preference is loaded. */
67
71
  export declare const DEFAULT_SCHEME: ThemeScheme;
68
72
  /**
@@ -96,6 +100,23 @@ export declare function isThemeScheme(value: string): value is ThemeScheme;
96
100
  * - `caution` — warning status tone.
97
101
  * - `alarm` — error/fault status tone.
98
102
  * - `pending` — busy/in-flight status tone.
103
+ *
104
+ * Rich-render roles (markdown / diff / syntax highlighting). These feed the
105
+ * framework adapter's markdown/diff/highlight accessors so the styled
106
+ * transcript, colored diffs, and fenced-code highlighting recolor for free with
107
+ * each scheme:
108
+ * - `codeInline` — inline `` `code` `` foreground.
109
+ * - `heading` — markdown heading foreground.
110
+ * - `blockquoteBar` — the dim bar drawn beside a blockquote.
111
+ * - `diffAddedBg` — background tint for an added (`+`) diff line.
112
+ * - `diffRemovedBg` — background tint for a removed (`-`) diff line.
113
+ * - `diffAddedText` — foreground for added-line content / `+` marker.
114
+ * - `diffRemovedText`— foreground for removed-line content / `-` marker.
115
+ * - `synKeyword` — syntax scope: keywords.
116
+ * - `synString` — syntax scope: string literals.
117
+ * - `synNumber` — syntax scope: numeric literals.
118
+ * - `synComment` — syntax scope: comments.
119
+ * - `synType` — syntax scope: types / classes / built-ins.
99
120
  */
100
121
  export interface ThemeTokens {
101
122
  readonly signal: string;
@@ -111,6 +132,18 @@ export interface ThemeTokens {
111
132
  readonly caution: string;
112
133
  readonly alarm: string;
113
134
  readonly pending: string;
135
+ readonly codeInline: string;
136
+ readonly heading: string;
137
+ readonly blockquoteBar: string;
138
+ readonly diffAddedBg: string;
139
+ readonly diffRemovedBg: string;
140
+ readonly diffAddedText: string;
141
+ readonly diffRemovedText: string;
142
+ readonly synKeyword: string;
143
+ readonly synString: string;
144
+ readonly synNumber: string;
145
+ readonly synComment: string;
146
+ readonly synType: string;
114
147
  }
115
148
  /** The closed set of token role names, for iteration and validation. */
116
149
  export type ThemeToken = keyof ThemeTokens;
@@ -211,8 +244,9 @@ export interface ViewRow {
211
244
  * - `signOut` — the provider sign-out confirmation.
212
245
  * - `oauth` — the in-flight OAuth device/redirect flow.
213
246
  * - `plugin` — a plugin-supplied select/confirm/input/custom overlay.
247
+ * - `approval` — a pending tool-permission prompt (allow once / always / deny).
214
248
  */
215
- export type ModalKind = "none" | "settings" | "models" | "scopedModels" | "theme" | "sessions" | "tree" | "userTurns" | "signIn" | "signOut" | "oauth" | "plugin";
249
+ export type ModalKind = "none" | "settings" | "models" | "scopedModels" | "theme" | "sessions" | "tree" | "userTurns" | "signIn" | "signOut" | "oauth" | "plugin" | "approval";
216
250
  /**
217
251
  * The active modal plus any opaque payload the matching dialog needs.
218
252
  *