@quantiya/codevibe-core 2.0.4 → 2.0.6

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.
@@ -7,6 +7,73 @@ import type { SlashCommandInfo } from '../slash-router';
7
7
  * dropdown can be open (Stage-2 — keeps the dropdown from re-triggering #469).
8
8
  */
9
9
  export declare const DROPDOWN_MAX_ROWS: number;
10
+ /**
11
+ * Single-width visible marker shown IN THE DISPLAY ONLY in place of a real
12
+ * newline. The value renders on ONE physical line (readline-style), so the real
13
+ * terminal cursor's row/column math is trivially single-line. The ACTUAL value
14
+ * keeps its real `\n` and is submitted verbatim — only the on-screen rendering
15
+ * single-lines. It is ONE code unit wide (like the chip `[`), so a cursor offset
16
+ * in the value maps 1:1 to a column in the display string (no caret drift).
17
+ *
18
+ * INTENTIONAL UX CHANGE (Hendry-approved, ink7 real-cursor fix): a multi-line
19
+ * (e.g. `\n`-bearing history-recall) value now shows inline on one row with the
20
+ * `⏎` marker + horizontal scroll, instead of wrapping to multiple rows. This is
21
+ * what makes the REAL terminal cursor land correctly in every terminal (the
22
+ * caret can no longer be on a wrapped/second row). Normal single-line typing —
23
+ * and the submitted value (newlines, chips) — are UNCHANGED.
24
+ */
25
+ export declare const NEWLINE_MARKER = "\u23CE";
26
+ /**
27
+ * Display form of a value: chip sentinels → `[` (displayChips) AND real newlines
28
+ * → the width-1 `NEWLINE_MARKER`. Both are 1:1 char substitutions, so
29
+ * `displayValue(v).length === v.length` and a cursor offset into the value indexes
30
+ * the same column in the display string.
31
+ */
32
+ export declare function displayValue(v: string): string;
33
+ /** Left border + left padding + prompt "❯ " width — the caret's x offset base. */
34
+ export declare const CARET_X_BASE = 4;
35
+ /** Top border row — the caret's y offset from the row Box's own top. */
36
+ export declare const CARET_Y_BASE = 1;
37
+ /**
38
+ * Pure caret-cell computation (unit-tested). `rowLeft`/`rowTop` are the input row
39
+ * Box's absolute top-left in Ink's dynamic output; `visibleCol` is the caret's
40
+ * ON-SCREEN column within the (single, horizontally-scrolled) value window
41
+ * (`caretDisplayCol − scroll`; see `computeInputWindow`). Returns the 0-based
42
+ * `{ x, y }` for `useCursor().setCursorPosition`. `y` is always one row (the
43
+ * value is one physical line).
44
+ */
45
+ export declare function caretPosition(rowLeft: number, rowTop: number, visibleCol: number): {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ /**
50
+ * Result of windowing the single-line display string so the caret stays on-screen.
51
+ * `text` is the ≤`valueWidth`-wide slice to render (`…` overwrites any clipped
52
+ * edge); `visibleCol` is the caret's column WITHIN that slice (feeds
53
+ * `caretPosition`); `scroll` is the chosen left offset (kept in a ref for sticky
54
+ * readline-style scrolling).
55
+ */
56
+ export interface InputWindow {
57
+ text: string;
58
+ visibleCol: number;
59
+ scroll: number;
60
+ }
61
+ /**
62
+ * Pure horizontal-scroll window for the single physical input line (unit-tested).
63
+ * `display` is the full display string (chips + newline-marker already applied, so
64
+ * its `.length` IS the display width, 1:1). `caretCol` is the logical caret column
65
+ * in `display` coords. `valueWidth` is the number of columns available for the
66
+ * value (= innerW − 2, where innerW = columns − border 2 − padX 2, and the `❯ `
67
+ * prompt takes the leading 2). `prevScroll` is the previous window offset (sticky).
68
+ *
69
+ * If everything fits (`display.length ≤ valueWidth`) the whole string shows with no
70
+ * scroll. Otherwise a `valueWidth`-wide window is slid over `display` so the caret
71
+ * is always visible — the previous scroll is only nudged when the caret crosses an
72
+ * edge (readline-style; the view does not jump). A `…` marks each clipped edge (a
73
+ * width-preserving overwrite of the boundary char — purely cosmetic; the real
74
+ * terminal cursor draws on top).
75
+ */
76
+ export declare function computeInputWindow(display: string, caretCol: number, valueWidth: number, prevScroll: number): InputWindow;
10
77
  /**
11
78
  * Gate-prompt-aware input modes per PHASE-CP-8-MIN-DESIGN.md §7
12
79
  * + #C8M-5. `maxOption: number` (NOT a `2 | 5` literal union) per
@@ -4,7 +4,7 @@ import { AppSyncClient } from '../appsync';
4
4
  * the Stage A contract. CP-1.a ships the union; CP-1.b/c/d add call
5
5
  * sites for each emit type.
6
6
  */
7
- export type NewEventTypeKind = 'MODE_SELECTED' | 'PLANNER_DECISION' | 'PLANNER_CACHE_HIT' | 'PLANNER_DEGRADED' | 'PLANNER_OUTAGE' | 'PLANNER_RECOVERED' | 'SLASH_COMMAND_INVOKED' | 'STRUCTURAL_SUMMARY_GENERATED' | 'LOCAL_AUTHORITY_REFUSAL' | 'NOTIFICATION' | 'ASSISTANT_RESPONSE';
7
+ export type NewEventTypeKind = 'MODE_SELECTED' | 'PLANNER_DECISION' | 'PLANNER_CACHE_HIT' | 'PLANNER_DEGRADED' | 'PLANNER_OUTAGE' | 'PLANNER_RECOVERED' | 'SLASH_COMMAND_INVOKED' | 'STRUCTURAL_SUMMARY_GENERATED' | 'LOCAL_AUTHORITY_REFUSAL' | 'NOTIFICATION' | 'ASSISTANT_RESPONSE' | 'USER_PROMPT';
8
8
  /**
9
9
  * §3 LOCK lines 160-170. ShellEventEmit contract.
10
10
  *
@@ -10,7 +10,7 @@ import { type RunAuditBrowserResult } from './audit-runner';
10
10
  import { Tier } from './types';
11
11
  import type { PendingClarification } from './types';
12
12
  import { type GatePromptEnvelope, type DecryptedReviewSummaryInput } from './gate-prompts';
13
- import type { Event } from '../types/events';
13
+ import type { Event, UpdateEventStatusInput } from '../types/events';
14
14
  import { type StructuralSummaryGenerator } from '../structural-summary';
15
15
  import type { PlannerAdapter } from '../planner/adapter';
16
16
  import type { PlannerCacheLayer } from '../planner/cache';
@@ -272,6 +272,61 @@ export declare const TASK_REVIEW_SUMMARY_FETCH_TIMEOUT_MS = 5000;
272
272
  */
273
273
  export type ReviewSummaryFetcher = (taskId: string, gateId: string, sessionId: string) => Promise<DecryptedReviewSummaryInput | null>;
274
274
  export declare function handleSubscribedEvent(event: Event, store: OrchestrationStore, sessionKeyResolver?: (sessionId: string) => Promise<string | null>, reviewSummaryFetcher?: ReviewSummaryFetcher, maybeAutoContinue?: (envelope: GatePromptEnvelope) => boolean): void;
275
+ /**
276
+ * Route a `USER_PROMPT` sent from the paired MOBILE companion into the shell's
277
+ * SERIALIZED submission pipeline so the planner processes it EXACTLY like a typed
278
+ * prompt (classify → decision → reply), and its reply mirrors back to the phone via
279
+ * `emitOrchestrationTurnMirror` (which suppresses the duplicate DESKTOP `USER_PROMPT`
280
+ * for a mobile-origin turn — see `skipUserPromptMirror`).
281
+ *
282
+ * Before this fix the shell subscribed to events but `handleSubscribedEvent` opened
283
+ * with a cheap `if (event.type !== 'INTERACTIVE_PROMPT') return;` pre-check, so a
284
+ * MOBILE `USER_PROMPT` ("Good morning") was delivered and silently DROPPED — the
285
+ * planner never saw it, the phone got no reply. This is the missing RETURN path
286
+ * (desktop→mobile mirroring already works via `emitOrchestrationTurnMirror`).
287
+ *
288
+ * CALLER CONTRACT — must pre-filter to `event.source === 'MOBILE' && event.type ===
289
+ * 'USER_PROMPT'`. The subscription runs with `receiveDesktopEvents:true`, so the
290
+ * shell's OWN Bug-A mirror emissions (DESKTOP `USER_PROMPT` / `ASSISTANT_RESPONSE`)
291
+ * are re-delivered here; feeding a DESKTOP echo back into `submit` would INFINITE-LOOP.
292
+ * Filtering to source===MOBILE (and only USER_PROMPT) is the anti-loop guard.
293
+ *
294
+ * The mobile `USER_PROMPT` content is E2E-encrypted (`isEncrypted:true`); decrypt it
295
+ * with the session key (the SAME resolver the non-gate prompt path uses) before
296
+ * submitting. Fail-soft: a missing resolver/key, a decrypt failure, or empty text logs
297
+ * and DROPS (never crashes the shell, never submits ciphertext). Async — the sync
298
+ * subscription callback fires-and-forgets via `void`; every throw is caught here.
299
+ */
300
+ export declare function routeMobileUserPrompt(event: Event, sessionKeyResolver: ((sessionId: string) => Promise<string | null>) | undefined, submit: (text: string, images?: string[], options?: ShellSubmitOptions) => Promise<void>, updateEventStatus?: (input: UpdateEventStatusInput) => Promise<unknown>): Promise<void>;
301
+ /**
302
+ * Build the single `(event) => void` callback the AppSync events subscription
303
+ * invokes. Routes each delivered event to exactly ONE handler:
304
+ *
305
+ * - MOBILE-source `USER_PROMPT` → `routeMobileUserPrompt` (the mobile→desktop RETURN
306
+ * path: decrypt + feed into the serialized submission pipeline). STRICT filter —
307
+ * see the anti-loop note below.
308
+ * - Everything else → `handleSubscribedEvent` (gate escalation / final-approval /
309
+ * continuation-handoff INTERACTIVE_PROMPTs + the agent's own AskUserQuestion).
310
+ *
311
+ * ANTI-LOOP (CRITICAL): the subscription runs with `receiveDesktopEvents:true`, so the
312
+ * shell's OWN Bug-A turn-mirror rows (DESKTOP `USER_PROMPT` + `ASSISTANT_RESPONSE`) are
313
+ * re-delivered here. The mobile branch fires ONLY for `source === 'MOBILE' && type ===
314
+ * 'USER_PROMPT'`, so a DESKTOP echo NEVER re-enters `submit` (which would infinite-loop);
315
+ * it falls through to `handleSubscribedEvent`, whose `type !== 'INTERACTIVE_PROMPT'`
316
+ * pre-check drops it (and `ASSISTANT_RESPONSE`/`MODE_SELECTED`/`NOTIFICATION`) unchanged.
317
+ *
318
+ * Extracted (vs an inline closure) so the routing decision is unit-testable in isolation.
319
+ * `submit` is passed as a thunk by the caller so the forward reference to the
320
+ * later-declared `handleUserInput` is only read at event time, never at setup.
321
+ */
322
+ export declare function createSubscribedEventDispatcher(deps: {
323
+ store: OrchestrationStore;
324
+ submit: (text: string, images?: string[], options?: ShellSubmitOptions) => Promise<void>;
325
+ sessionKeyResolver?: (sessionId: string) => Promise<string | null>;
326
+ reviewSummaryFetcher?: ReviewSummaryFetcher;
327
+ maybeAutoContinue?: (envelope: GatePromptEnvelope) => boolean;
328
+ updateEventStatus?: (input: UpdateEventStatusInput) => Promise<unknown>;
329
+ }): (event: Event) => void;
275
330
  /**
276
331
  * Route ONE team-mode LE shell event into the store's minimal `team` state.
277
332
  * The W2.b `emitTrackProgress` emits a NOTIFICATION carrying
@@ -631,7 +686,21 @@ export interface HandleShellUserInputDeps extends StructuralSummaryRunDeps {
631
686
  * real result/rejection), while the internal queue swallows rejections so one
632
687
  * failed turn can't break serialization for later submissions.
633
688
  */
634
- export declare function serializeShellSubmissions(handler: (text: string, images?: string[]) => Promise<void>): (text: string, images?: string[]) => Promise<void>;
689
+ /**
690
+ * Options threaded through a shell submission so the turn handler + the companion
691
+ * mirror can distinguish a mobile-origin turn from a keyboard turn.
692
+ */
693
+ export interface ShellSubmitOptions {
694
+ /**
695
+ * The submission originated from the paired MOBILE companion (the mobile→desktop
696
+ * RETURN path). The turn mirror (`emitOrchestrationTurnMirror`) must NOT re-emit a
697
+ * DESKTOP `USER_PROMPT` for this turn — the phone already displays its own sent
698
+ * message — but MUST still mirror the `ASSISTANT_RESPONSE`. Absent/false ⇒ a normal
699
+ * keyboard turn (both `USER_PROMPT` + `ASSISTANT_RESPONSE` mirror as today).
700
+ */
701
+ fromMobile?: boolean;
702
+ }
703
+ export declare function serializeShellSubmissions(handler: (text: string, images?: string[], options?: ShellSubmitOptions) => Promise<void>): (text: string, images?: string[], options?: ShellSubmitOptions) => Promise<void>;
635
704
  /**
636
705
  * Process one shell submission (slash command or natural-language → planner).
637
706
  *
@@ -668,6 +737,54 @@ export declare function buildContinuationActionDeps(args: RunOrchestrationShellA
668
737
  */
669
738
  export declare function detectBrowseFastPath(text: string, pending: PendingClarification | null): string[] | null;
670
739
  export declare function handleShellUserInput(deps: HandleShellUserInputDeps): Promise<void>;
740
+ /**
741
+ * COMPANION-MIRROR (dogfood 2026-07-05) — mirror ONE just-completed conversational
742
+ * (natural-language) turn to the backend so the paired mobile companion can SEE the
743
+ * orchestration chat.
744
+ *
745
+ * The orchestration shell previously emitted only MODE_SELECTED (+ NOTIFICATION
746
+ * audits + the team-result ASSISTANT_RESPONSE) to the backend — a plain `"hi"` →
747
+ * planner-greeting turn produced NO `USER_PROMPT` / `ASSISTANT_RESPONSE`, so the phone
748
+ * showed nothing for conversational turns. This mirrors, per turn, via the SAME
749
+ * cohort-gated + E2E-encrypted `emitShellEvent` chokepoint MODE_SELECTED uses:
750
+ * - USER_PROMPT — the user's submitted text (this turn's `user-message` entry)
751
+ * - ASSISTANT_RESPONSE — the planner/shell conversational reply rendered this turn
752
+ *
753
+ * Scope = CONVERSATIONAL (NON-team) turns:
754
+ * - No `user-message` appended this turn (slash command / empty input) ⇒ nothing to
755
+ * mirror (return early — slash commands already emit their own SLASH_COMMAND_INVOKED).
756
+ * - This turn was a `start_task` / `team_decompose` decision ⇒ the reply is OWNED by
757
+ * the team-result path (`emitGroupConvergence` → `leEmitShellEvent`, which emits its
758
+ * OWN ASSISTANT_RESPONSE when the team converges — cli.ts `mapTeamLeEventToShellEmit`).
759
+ * We emit ONLY the USER_PROMPT and leave the reply to that path — NO double-emit.
760
+ * The reducer ALWAYS appends a (TUI-hidden) `planner-decision` entry, so the delta
761
+ * carries the action even though the team launch's TASK_LIFECYCLE may land async.
762
+ *
763
+ * Ordering: USER_PROMPT carries the turn-start timestamp and ASSISTANT_RESPONSE a
764
+ * later `new Date()` (classify took real time), so the companion's (timestamp,id) sort
765
+ * renders prompt-before-reply.
766
+ *
767
+ * Fail-soft: an emit throw is logged, never fatal to the shell — matching the sibling
768
+ * MODE_SELECTED emit. The stub-session refusal + cohort gate + no-session-key
769
+ * fail-closed all live INSIDE `emitShellEvent`; we route through it (never bypass it)
770
+ * and pass PLAINTEXT `content` (the chokepoint encrypts under the session key).
771
+ */
772
+ export declare function emitOrchestrationTurnMirror(deps: {
773
+ store: OrchestrationStore;
774
+ sessionId: string;
775
+ emitShellEventBound: ReturnType<typeof createShellEventEmitter>;
776
+ /** Conversation length captured BEFORE this turn ran (the delta baseline). */
777
+ convLenBefore: number;
778
+ /** RFC3339 timestamp captured at turn start — the USER_PROMPT event time. */
779
+ userTurnTimestamp: string;
780
+ /**
781
+ * MOBILE→DESKTOP RETURN PATH — this turn originated from the paired MOBILE companion,
782
+ * so the phone ALREADY displays its own sent message. Skip the DESKTOP `USER_PROMPT`
783
+ * mirror (it would duplicate on the phone) but STILL emit the `ASSISTANT_RESPONSE` so
784
+ * the planner's reply reaches mobile. Absent/false ⇒ a keyboard turn (mirror both).
785
+ */
786
+ skipUserPromptMirror?: boolean;
787
+ }): Promise<void>;
671
788
  /**
672
789
  * CP-4 §4.2 (Codex r1 M1 — the double-walk unify) — the SINGLE structural-
673
790
  * summary refresh entry point. Drives the context store via
@@ -2,7 +2,11 @@ interface InkExports {
2
2
  Box: any;
3
3
  Text: any;
4
4
  Static: any;
5
- render: (tree: any) => {
5
+ render: (tree: any, options?: {
6
+ stdout?: any;
7
+ stdin?: any;
8
+ stderr?: any;
9
+ }) => {
6
10
  waitUntilExit(): Promise<void>;
7
11
  unmount(): void;
8
12
  };
@@ -12,6 +16,12 @@ interface InkExports {
12
16
  useInput: (handler: (input: string, key: any) => void, options?: {
13
17
  isActive?: boolean;
14
18
  }) => void;
19
+ useCursor: () => {
20
+ setCursorPosition: (position: {
21
+ x: number;
22
+ y: number;
23
+ } | undefined) => void;
24
+ };
15
25
  useStdin: () => {
16
26
  stdin: any;
17
27
  setRawMode: (raw: boolean) => void;
@@ -20,23 +20,19 @@ export interface ParsedModeArgs {
20
20
  * callback instead of opening a real TTY.
21
21
  */
22
22
  export interface ModeSelectionDeps {
23
- readStickyPreference: (tier: Tier) => Promise<Mode | null>;
24
- writeStickyPreference: (args: {
25
- mode: Mode;
26
- pickedByTier: 'PRO' | 'MAX';
27
- }) => Promise<void>;
28
23
  promptUserForMode: (tier: 'PRO' | 'MAX') => Promise<Mode>;
29
24
  }
30
25
  /**
31
- * §2 LOCK lines 80-99. Pure decision function — only filesystem I/O is
32
- * the sticky-preference read via `deps.readStickyPreference`.
26
+ * §2 LOCK lines 80-99. Decision function — Pro/Max always resolve via an
27
+ * interactive prompt (`deps.promptUserForMode`); no sticky preference is
28
+ * read or written here.
33
29
  *
34
30
  * Rules:
35
31
  * 1. Free-tier + `--mode orchestration` → `TierError` with upgrade URL.
36
32
  * 2. Explicit `--mode` flag → honor it.
37
33
  * 3. Free-tier (no flag) → 'companion'.
38
- * 4. Pro/Max (no flag) + sticky present → sticky mode.
39
- * 5. Pro/Max (no flag) + no sticky → prompt user; write sticky after pick.
34
+ * 4. Pro/Max (no flag) → always prompt the user (sticky preference is
35
+ * never consulted; the answer is not persisted).
40
36
  */
41
37
  export declare function pickMode(args: ParsedModeArgs, tier: Tier, deps?: ModeSelectionDeps): Promise<Mode>;
42
38
  /**
@@ -13,7 +13,8 @@ export declare enum SessionStatus {
13
13
  export declare enum AgentType {
14
14
  CLAUDE = "CLAUDE",
15
15
  GEMINI = "GEMINI",
16
- CODEX = "CODEX"
16
+ CODEX = "CODEX",
17
+ CODEVIBE = "CODEVIBE"
17
18
  }
18
19
  /**
19
20
  * Session type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-core",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Core library for CodeVibe plugins - shared keychain, crypto, AppSync, and auth functionality",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,13 +28,14 @@
28
28
  "@iarna/toml": "^2.2.5",
29
29
  "@npmcli/package-json": "^5.2.1",
30
30
  "gradient-string": "^3.0.0",
31
- "ink": "5.2.1",
31
+ "ink": "7.1.0",
32
32
  "ink-select-input": "^6.2.0",
33
33
  "ink-spinner": "^5.0.0",
34
34
  "ink-text-input": "^6.0.0",
35
35
  "json-freeze": "^1.0.0",
36
36
  "parse5": "^7.3.0",
37
- "react": "^18.3.1",
37
+ "react": "^19.2.0",
38
+ "react-devtools-core": "^6.1.2",
38
39
  "ulid": "^3.0.0",
39
40
  "uuid": "^9.0.0",
40
41
  "ws": "^8.14.0",
@@ -47,7 +48,7 @@
47
48
  "devDependencies": {
48
49
  "@types/gradient-string": "^1.1.6",
49
50
  "@types/node": "^20.0.0",
50
- "@types/react": "^18.3.0",
51
+ "@types/react": "^19.2.0",
51
52
  "@types/uuid": "^9.0.0",
52
53
  "@types/ws": "^8.5.0",
53
54
  "esbuild": "^0.28.0",
@@ -56,7 +57,7 @@
56
57
  "vitest": "^1.6.1"
57
58
  },
58
59
  "engines": {
59
- "node": ">=18.0.0"
60
+ "node": ">=22.0.0"
60
61
  },
61
62
  "author": "Quantiya",
62
63
  "license": "MIT",