Commit Graph
200 Commits
Author SHA1 Message Date
ZacharyZhang-NY aba2dac3c5 fix(docs): stop leaking backend reason codes to the open-file toast
The previous toast pasted result.error.message straight into the UI,
so a failed open rendered
  "Couldn't open CEO-WEEK2-DAY1-SUMMARY.md: workspace_id=null
  task_id=null run_id=61669170-180b-4cb3-99d2-eb925ca9717c — none led
  to a workspace with a local_path"
to the operator. That text is a developer log line, not user feedback.

Drop the passthrough — the toast now simply reads
  "Couldn't open <filename>"
and the diagnostic details live only in the IPC return payload (which
a devtools or automated caller can still read). The resolver's five
reason codes and their detail strings remain intact in the main
process; they just no longer surface to the UI.
2026-04-21 14:58:39 -04:00
ZacharyZhang-NY 9f43549606 fix(docs): surface work-product open failures with a toast
Clicking a Files & Deliverables row fired openWorkProduct and then
discarded the result with \`void\`. When the main process could not
resolve the target — archived work product, missing workspace root,
path outside the managed root, or \`shell.openPath\` reporting no
associated app for the file type — the renderer stayed silent and
the row looked like a dead button. The user had no way to know why
"clicking" did nothing.

Await the result in WorkProductRow and route non-ok responses through
addToast with tone="danger". The caller sees the underlying failure
(OPEN_PATH_FORBIDDEN / OPEN_PATH_FAILED / WORK_PRODUCT_NOT_FOUND /
the shell's own message) and can act on it. No backend change — the
handler already returned structured errors; this just stops dropping
them.

Typecheck and build:verify clean.
2026-04-21 14:42:00 -04:00
ZacharyZhang-NY 7301b00cb2 fix(design): actually apply org chart color-bar cleanup
The previous commit 86e9c21 only caught the heartbeat icon color —
the main block of edits silently failed to match (comment block
broke the exact-string diff). This commit lands the full cleanup
that commit 86e9c21 claimed:

- Drop the rounded-t [4px bg-primary | 2px bg-border] rank bar.
- Drop the isRunning border-[color:var(--primary)] override and the
  success-tinted box-shadow glow.
- Switch hover from shadow-lg + translate-y to hover:bg-muted/30.
- Switch active/running avatar circle from bg-success-muted /
  text-success to bg-accent / text-primary.
- Move avatar initials from font-bold to font-semibold.
- Drop the isRoot / stale rank-bar comment block.

Error and pending states keep their destructive borders.

Typecheck and build:verify clean.
2026-04-21 14:38:53 -04:00
ZacharyZhang-NY 86e9c215f1 refactor(design): strip color bars from org chart cards
Remove the ink-blue top rank bar, running-state border override, and
green glow shadow from org chart cards. The tree layout already
communicates rank by position, and a dashed ink-blue connection line
plus the bottom-right status dot + ping animation already mark
running agents — duplicating the signal through a card-wide color
band and a cyan/green shadow was pure decoration.

- Drop the rounded-t [4px bg-primary | 2px bg-border] top bar entirely.
- Running state stops overriding border to var(--primary); all running
  cards fall back to the neutral border-border like everyone else.
- Running state stops emitting a success-tinted glow (shadow-[0_0_12px
  color-mix(... var(--success) 15% ...)]) — kami rule 8 bans drop
  shadows, and the tint introduced an unauthorized green.
- Hover switches from shadow-lg + translate-y-0.5 to a subtle
  hover:bg-muted/30 tint.
- Active/running avatar circle switches from bg-success-muted
  text-success to bg-accent text-primary (kami's ink-blue tint),
  removing the last green chrome on the page.
- Heartbeat icon switches from text-success to text-primary.
- Avatar initials move from font-bold (700) to font-semibold (600).

Error / pending_approval keep their destructive border (red), since
kami reserves the warm terra red for genuine error/destructive state
per design.md color system.

Typecheck and build:verify clean.
2026-04-21 14:38:00 -04:00
ZacharyZhang-NY a5caecbfda refactor(design): remove multi-hue color bars, unify to ink blue
The kanban board, performance metric cards, workspace list, and wait
banner all leaked second and third chromatic hues into the UI: red
bars on critical tasks, amber on in-review columns, green on done /
running / selected workspace rows, colored icon tiles and colored
headline values on metric cards. kami rule 2 allows one accent (ink
blue) at no more than 5% of the surface; these surfaces were spending
30-60%.

KanbanBoard.tsx
  - Drop the tone/dotColor fields from KANBAN_COLUMNS; column headers
    are plain eyebrows, no colored status dot.
  - Delete priorityColors map and border-l-[3px] on cards; cards are
    flat bordered rectangles. Priority order still drives the sort,
    but stops painting the chrome.
  - CRITICAL/HIGH small-caps label routes through text-primary (ink
    blue) instead of text-destructive / text-warning.
  - "Running" status dot + text switches from text-success / bg-success
    to text-primary / bg-primary.
  - Hover changes from hover:shadow-sm (banned by kami rule 8) to
    hover:bg-muted/40.

MetricsGrid.tsx + Dashboard.tsx
  - Drop the tone prop and its toneMap / bgMap / borderMap trio. A
    MetricCard renders one way: ivory card, ink-blue-tinted icon tile
    (bg-accent), ink-blue glyph, near-black value. No left border
    color stripe, no hover shadow, no translate-y.
  - Dashboard.tsx stops passing tone on the four performance cards.
    Semantics that used to live in tone (pending approvals warn, all
    agents running success) are intentionally dropped; those
    affordances can come back through typography or a subtle indicator
    when kami allows, not through a second color.

WorkspaceList.tsx / WorkspaceInspector.tsx
  - Replace every border-l / text / group-hover reference to var(--success)
    with var(--primary). Selected + hover states now carry kami's ink
    blue signal; the three "X count" spans that used text-success as
    a decorative pop become text-primary for the same reason.

WaitStateIndicator.tsx
  - The wait banner (wait-for-reply / approval) flips from warning
    amber (border-warning, bg-warning-muted, text-warning, bg-warning
    button) to primary ink blue across border / bg / text / button.
    The banner still reads as an attention signal — the icon shape
    (Hourglass) carries the semantic, color no longer has to shout.

feedback.tsx (Alert) and Toast.tsx retain their tone-based colors:
they are accessibility-critical transient state surfaces where a
single ink-blue lane would collapse success / warn / danger into an
undifferentiated stack. They are out of scope for this pass.

Typecheck and build:verify clean. Grep confirms no remaining
border-l-[color:var(--success|warning|destructive|caution)] in
dashboard or execution modules.
2026-04-21 14:34:36 -04:00
ZacharyZhang-NY 86ca568b90 refactor(inbox): kami literary rewrite — drop tiles, cards, red stripe
The Inbox rendered every row as a heavy card: a 36x36 solid-fill icon
tile in ink-blue / destructive / warm-silver, a rounded border, a 3pt
red left stripe on urgent items, and a hover that fired a drop shadow
plus translate-y. Six loud signals, all fighting for the reader, all
breaking kami rules (no pure-hue tiles, no red accents outside
destructive buttons, no hard drop shadows, no unnecessary chrome).

Per Jony + Jobs consensus the surface collapses to one quiet canvas:

  - 36x36 color tile  -> removed. Icon is an 18px inline glyph, single
    ink-blue stroke for every type. Type meaning lives in the glyph
    shape (ShieldCheck / XCircle / Eye / ChatCircle / ...), not in
    background color.
  - 3pt red severity stripe -> removed. Urgency compresses to one
    signal: a 5px ink-blue dot before the timestamp, visible only for
    critical/high rows. Title also shifts from font-medium to
    font-semibold at the same threshold so the row carries a little
    more weight without shouting.
  - Card chrome (border, rounded, bg-card) -> removed. Rows sit flat
    on parchment, separated by a single border-b at --border/60
    hairline. Reads like a printed contents page instead of a trello
    board of cards.
  - Hover shadow-md + -translate-y-px -> removed (kami rule 8 bans
    hard drop shadows). Hover tints the row with bg-muted/40, which
    is the kami whisper-shadow equivalent for a list item.

AttentionRow gains a variant prop ("card" | "line", default "card")
so Dashboard's three existing call-sites keep their card rendering
untouched. The "line" branch rejects iconTileBg / rowBgClass /
borderAccentClass overrides — the two variants are orthogonal, not a
half-mixed code path.

inbox-helpers.tsx sheds every token that became dead after the
rewrite: typeConfig.color (constant var(--primary) for every type
after Jony's unification), severityColors, severityBorderClass, and
the unused bg field. Inbox routes icon color through tone="info"
instead of passing the redundant iconColor per row.

Note: the page shows 6 duplicate "Escalation from Maya Rodriguez"
rows and 5 duplicate "Escalation from Taylor Park" rows. That is a
backend dedup bug (same entity + same severity pushed multiple
times), not a visual issue. Flag for a dedicated follow-up; do not
paper over it in the renderer.

Reviewed by Karpathy against /karpathy-guidelines:
  - Surgical: pass
  - No overcomplication: pass
  - Variants orthogonal: pass
  - Orphans cleared: pass
  - API stability for Dashboard callers: pass
  - request_changes item (typeConfig.color is a constant dressed as
    data) addressed in this commit by dropping the field and routing
    through tone="info".

Typecheck and build:verify clean.
2026-04-21 14:25:26 -04:00
ZacharyZhang-NY f9b505569c fix(design): default badge tone becomes ink blue, readable in light mode
The previous default tone (bg-muted text-muted-foreground) reads as
beige-on-beige in light mode — the "3" count on Work was barely
legible against the parchment sidebar.

Route default counts through bg-primary + text-primary-foreground so
the pill adopts kami's sole accent color. The three tones now fall
into a clean priority scale:

  default -> ink blue        (you have items)
  warn    -> warm amber      (needs attention)
  danger  -> warm terra red  (critical)

Contrast holds in both modes: ink-on-parchment in light (~9:1),
ink-light-on-dark-parchment in dark (~5:1). Warm amber and terra red
retain their own semantic foregrounds from the prior commit.

Typecheck and build:verify clean.
2026-04-21 14:12:49 -04:00
ZacharyZhang-NY dc6744bf71 fix(design): make sidebar badges readable in dark mode
Badge text color was hard-coded to text-primary-foreground for every
tone, which renders as parchment light. Pairing light text with the
amber warning background (L=0.82) or the warm-silver muted background
(L=0.77) produced light-on-light collisions — the "9" on the active
Messages row and the "4" count on Work were nearly unreadable.

Route each tone through its own semantic foreground token:

  danger  -> bg-destructive text-destructive-foreground
  warn    -> bg-warning     text-warning-foreground    (dark text on amber)
  default -> bg-muted        text-muted-foreground

Also pull .dark --muted and --secondary off the --card lightness
(0.296 -> 0.360) so bg-muted is actually visible against the sidebar
card surface. This fixes not just the count pill but every hover/muted
fill across the app (toggle rest state, subtle button backgrounds, etc)
that previously vanished into the card.

Typecheck and build:verify clean.
2026-04-21 14:09:44 -04:00
ZacharyZhang-NY 72851ebea3 fix(design): unify messages canvas, keep only app nav elevated
On the Messages page three surfaces collided: the channel list column
and the channel header bar both used bg-card (lighter surface), while
the content area above and below used bg-background (deep). That made
the page read as a sandwich of alternating tones.

Route ChannelSidebar and ChannelHeader through bg-background so the
entire Messages canvas — title bar, channel list, header bar, and
conversation pane — shares the deep kami surface. The left app
SidebarView continues to use bg-card as the single elevated island,
preserving the apple-style "one navigation column floats on the
canvas" hierarchy.

Borders (border-r / border-b on those two panels) already provide the
structural separation, so no shadow is needed — consistent with kami's
rule 8 (ring or whisper shadow only).

Typecheck and build:verify clean.
2026-04-21 13:53:30 -04:00
ZacharyZhang-NY 38a6b82d13 fix(design): warm the dark mode so parchment olive actually reads
The previous dark palette set chroma near zero (C=0.003) on hue 95, which
is technically "warm" but visually indistinguishable from neutral black
at low luminance. Screens rendered the surface as pure black, losing the
"暖橄榄底色" kami promises.

Raise chroma to 0.013-0.014 and shift hue from 95 (cool yellow) toward
80 (closer to olive/gold) so the warmth is perceivable. Lift the
background L from 0.156 to 0.172 so it sits one step off true black.
Separate card (0.296) from border (0.340) so elevation reads without
a hard drop shadow.

Specifically:
  --kami-deep-dark/--kami-near-black  0.156 C0.003 H95 -> 0.172 C0.013 H80
  --kami-dark-surface                 0.283 C0.003 H95 -> 0.296 C0.014 H80
  --kami-dark-warm                    0.336 C0.004 H95 -> 0.350 C0.014 H80
  --kami-border-dark                  0.283 C0.003 H95 -> 0.340 C0.014 H80
  --kami-warm-silver                  0.739 C0.010 H95 -> 0.770 C0.015 H85

Dark surface / border / text tokens flow through the same shifts:
  .dark --background / --sidebar       0.172 warm
  .dark --card / --popover / --secondary / --muted / --sidebar-accent  0.296 warm
  .dark --border / --input / --sidebar-border  0.340 warm (visible)
  .dark --muted-foreground / --secondary-foreground / --sidebar-foreground
                                     0.770 warm (readable timestamps/role labels)
  .dark --accent                       0.320 warm ink hint (selection glows ink)

Light mode --foreground / --card-foreground / --popover-foreground /
--warning-foreground inherit the new warmer near-black so body text on
parchment also carries the olive signature.

Typecheck and build:verify clean.
2026-04-21 13:48:33 -04:00
ZacharyZhang-NY 768fd34e98 feat(design): apply kami serif 500 to app headings
Extend the @layer base h1/h2/h3 rule so every application-level heading
inherits the kami serif stack at weight 500, matching design.md rule 4
(serif carries headline authority) and rule 5 (single serif weight is
the design signature). Prose headings inside .paperclip-markdown already
inherit the same stack via the prose rules added in the token rewrite.

Components that override font-family/font-weight on headings will
continue to win via CSS specificity; the base rule sets the kami default
for every heading that does not opt out.

Typecheck and build:verify clean.
2026-04-21 13:39:36 -04:00
ZacharyZhang-NY 95c4b0ae48 refactor(design): migrate hardcoded colors to kami tokens
Purge remaining hardcoded color values from the component layer and main
process so every surface flows through the kami token system.

src/main/desktop-shell-window.ts
  - BrowserWindow.backgroundColor no longer paints pure white before the
    renderer mounts. It now picks kami parchment (#F5F4ED) or deep-dark
    (#141413) based on nativeTheme.shouldUseDarkColors so the startup
    flash respects the user's OS theme preference.

src/shared/constants.ts
  - DEPARTMENT_COLORS collapses 12 unrelated hex brand colors (violet,
    red, green, cyan, pink, ...) into six kami tokens: var(--primary),
    var(--kami-ink), var(--kami-dark-warm), var(--kami-charcoal),
    var(--kami-olive), var(--kami-stone). Departments are distinguished
    by warm-neutral ramp + ink-blue, never by second chromatic hue.

src/renderer/components/communication/{MessageItem,SearchResults}.tsx
  - Replace the string-concat tint pattern `${color}18` with
    color-mix(in oklab, ${color}, transparent 91%). The old pattern
    produced invalid CSS (e.g. `var(--primary)18`) whenever `color`
    was a CSS custom property, silently falling back to no background.
    color-mix works for both hex and var() inputs, eliminating the
    latent bug at the source.

src/renderer/components/ui/ActionButton.tsx
  - Replace hand-rolled rgba(74,93,122,0.35) and rgba(220,53,69,0.3)
    drop shadows with kami ring shadows routed through color-mix over
    var(--primary) and var(--destructive).

src/renderer/views/CommandPaletteView.tsx
  - Replace rgba(15,23,42,0.08) cold-slate shadow with a kami whisper
    shadow driven by color-mix over var(--foreground).

src/renderer/components/skills/SkillAgentToggles.tsx
src/renderer/components/execution/ModelManagementShared.tsx
  - Toggle thumbs switch from bg-white to bg-background so they
    inherit parchment in light mode and deep-dark in dark mode,
    removing the last pure-white surfaces from the renderer.

Third-party brand hex values in src/renderer/components/social/
social-config.ts are retained: they represent external brand identity
(Twitter blue, YouTube red, ...), not kami chrome, and collapsing them
to one hue would destroy recognition.

Typecheck clean. No business-logic signatures changed.
2026-04-21 13:38:06 -04:00
ZacharyZhang-NY 5113abacc7 feat(design): adopt kami warm-parchment + ink-blue token system
Rewrite styles.css token layer to enforce the kami design system across
light and dark modes:

- Replace grayscale oklch(L 0 0) palette with warm-parchment hue 95
  neutrals and ink-blue hue 258/255 brand; zero cold blue-gray remains.
- Introduce --kami-* source-of-truth layer (parchment/ivory/warm-sand,
  deep-dark/dark-surface, ink/ink-light, warm neutrals, borders, rings,
  ink-blue tag tints) so future components reference semantic names.
- Keep shadcn aliases (--background/--primary/--card/...) so consumer
  components require no className changes; kami is a drop-in under the
  token layer.
- Reset radius-lg/xl from 0 to 0.5rem/0.75rem (kami 8pt/12pt); default
  --radius 0 -> 0.5rem.
- Reset font stacks: serif -> Newsreader + Source Han Serif SC + fallbacks
  for headlines/body; sans -> Inter + system + Source Han Sans for UI;
  mono -> JetBrains Mono + Maple Mono + ui-monospace for code. Maple Mono
  no longer occupies the sans-primary slot.
- Collapse chart-1..5 to a single ink-blue hue (258->252) with L ramp
  per Jobs ruling; dark mode inverts the L order for contrast.
- Desaturate success/warning/caution to warm hues (145/75/50);
  destructive retains warm terra red #B53333 (oklch(0.516 0.186 25)).
- Replace MDXEditor Catppuccin palette (#1e1e2e/#cdd6f4/#181825/
  #585b70/#313244/#45475a/#89b4fa) with kami dark-surface + parchment
  + olive + ink-light equivalents.
- Route markdown link colors through var(--primary) via color-mix
  instead of hardcoded #0969da/#58a6ff.
- Enforce serif 500 (not 700) on .paperclip-markdown h1-h4 per kami
  rule 5 (single serif weight is the design signature).

CSP font-src is self/data only, so no CDN fonts are introduced; the
stacks rely on system-installed fonts with documented fallbacks.

Self-checks pass: no remaining oklch(L 0 0) grayscale, no Catppuccin or
GitHub link literals, no font-weight 700 on prose headings, no radius: 0
in @theme, Maple Mono only lives in the mono fallback chain.
2026-04-21 13:32:40 -04:00
ZacharyZhang-NY 97033dfabd update 2026-04-21 12:44:33 -04:00
ZacharyZhang-NY af72f1b4d9 Make @mentions in a DM redirect the turn (group-chat semantics)
The operator's mental model: a chat is a room, @X means "X takes the
turn", unmentioned participants keep working. Two separate gaps were
letting 张工程师 answer when the user wrote "@CEO 更新一下进度" inside
their Board↔张工程师 chat:

* `resolveMessageWakeTargets` always woke the DM recipient, regardless
  of who the body @-mentioned. CEO woke too, but 张工程师 — the
  addressed party — replied first and CEO's busy-queued wake arrived
  after the operator had already taken the earlier reply as "the
  answer". Explicit mentions now shadow the implicit DM recipient:
  with mentions, only the named agents wake; the recipient still sees
  the message in their inbox, they just aren't obliged to answer.
  No mentions → fall back to the historical "DM wakes the recipient"
  contract so every existing flow is untouched.

* `channel-utils` keyed each chat by `sort(fromAgent, toAgent)`. CEO's
  reply with `toAgentId=__board__, parentMessageId=<original>` landed
  in a fresh Board↔CEO pane — the exact "new session" the operator
  complained about. Walk `parentMessageId` back to the root of the
  reply chain and use the root's participant pair: every reply in a
  thread now collapses into the chat the sender originally opened,
  cycle-guarded against malformed parent pointers.

Pinned by a new unit test: DM to CTO with "@CEO 更新一下进度。" wakes
only CEO while keeping the message visible to both CTO's and CEO's
inbox.
2026-04-20 08:59:14 -04:00
ZacharyZhang-NY a99f5e581f Fix stuck selectedEntityId and unread-during-search edge cases
Two small but user-visible UX issues in the Communication hub:

1. selectedEntityId was only consumed when the id resolved to a
   visible message / comment / task channel. When the referenced
   entity was deleted or filtered out, the pointer stuck and the
   effect re-ran on every store change, preventing the user from
   manually navigating to other channels.

2. The unread-marking effect explicitly skipped while search was
   active, even though the search results render in the same message
   list pane. Users who opened a channel via search saw unread
   badges freeze until they cleared the query and re-entered the
   channel.

Consume the entity id on every resolution branch (including the
no-match fallback) and drop the search guard — the viewer intent is
the same in both list modes.
2026-04-19 19:29:23 -04:00
ZacharyZhang-NY 2222a67428 Stop rendering the DocumentCenter as a blank page
Both the prose-documents and work-products sections rendered as
`{x.length > 0 ? ... : null}` with no else branch, and the whole
DocumentCenter was only mounted when a company was selected. The
combined result: a company with no files gave the operator a page
containing only a title and a "New" button, and an operator who had
not yet picked a company saw nothing at all — the exact "page is blank
even though docs were generated" complaint.

Add an explicit empty state inside DocumentCenter (surfaced only when
both collections are empty) and a separate "select a company" empty
state at the section router level. Strings live in the i18n bundle so
the Chinese locale is not left behind.
2026-04-19 19:10:57 -04:00
ZacharyZhang-NY ff9a52324e Route work-product clicks through a resolver IPC
Filesystem work products store `external_id` as a workspace-relative
path so artifacts survive a workspace-root move, but the renderer was
handing that relative fragment straight to `shell.openPath`, which
needs an absolute managed path. Clicks flickered into a failed open.

Add a dedicated `openWorkProduct({ companyId, id })` IPC backed by
`resolveWorkProductTarget` on the DB layer. The resolver joins the
work-product row back to its originating task → workspace (or run →
workspace for taskless runs), composes an absolute path, and rejects
any relative fragment that would escape the workspace root. The
renderer now passes the work product's id — path resolution stays on
the main process where the managed-path policy already lives.
2026-04-19 15:05:21 -04:00
ZacharyZhang-NY 22ba7a9004 feat(documents): surface filesystem work-products in Document Center
The Document Center only read the `documents` table, which is prose
content POSTed via /api/documents. Everything the agents wrote via
`bash` / `write` / `edit` — the bulk of their output on a real
project — landed in `work_products` and never appeared on the page.
Operators saw "0 items" after a full night's run.

Lift work_products into the same UI:

  - `listWorkProductsForCompany` returns a newest-first slice with a
    500-row cap so the snapshot stays bounded.
  - `ProfileSnapshot.workProducts` flows through `useCompanyData` as
    `companyWorkProducts`, filtered to the current company.
  - `DocumentCenter` renders two labelled sections in order — prose
    documents (editable) on top, files & deliverables below. Each
    work-product row opens the underlying URL or filesystem path
    when clickable, so the agent's bash-generated markdown, PRs,
    and artifacts are one click away.

`WorkProductRecord` lifts into `@shared/types/records-extended` so
the renderer, snapshot, and database shells all reference the same
shape. The store-test fixture learns about the new field.
2026-04-19 13:24:50 -04:00
ZacharyZhang-NY 0746664f72 feat(review-panel): split reviews by author — board / human / manager
One flat list of `status = in_review` tasks couldn't tell the
operator whether the next decision belonged to the board (approval
pending), to the human user (top-level assignee or explicit
`requires_user_review`), or to a middle manager walking up the
`reports_to` chain. Three very different escalation paths rendered
identically, so "reviewed by X" was the only signal and operators
missed the ones actually waiting on them.

`PendingReviewList` now classifies each in-review task into one of
three kinds (`board` / `user` / `manager`) using the pending
approval set plus the existing assignee/manager lookup. Rendering
groups them under labelled sub-headings in priority order (board
first — the operator is the bottleneck there), each with its own
awaiting-text so the row's status is unambiguous at a glance.

The component accepts approvals via prop; the caller passes the
existing `pendingApprovals` it already holds. Translations for both
locales carry the new group labels.
2026-04-19 01:24:01 -04:00
ZacharyZhang-NY 0a40c55b3c fix(thread-panel): wire the scoped reply composer to real participants
The thread-panel composer was pinned to `channel="direct"` with no
`directParticipantIds` and no `replyTo`. Clicking reply in a human
review thread sent a message with no recipient and the send button
stayed disabled — a silent dead-end for the entire Co-Founder
review UX.

The fix exposes the thread's participant list to the renderer:

  - `listThreadParticipantsSchema` and the matching IPC channel
    (`agent-company:list-thread-participants`) return the rows from
    `conversation_participants` for a given thread.
  - `threadIntegration.onTaskCreated` / `onApprovalCreated` already
    enrol the owning agent and the virtual user on thread creation,
    so the list is populated without any backfill.
  - `ThreadPanel` loads participants alongside events and forwards
    the agent ids to `MessageComposer.directParticipantIds`. The
    composer's existing `directRecipientId` memo picks the first
    non-board agent, so replies now target the correct agent — or
    surface the "no recipient" notice when the thread has none.

The Board/user member is deliberately excluded from the list the
composer consumes: the composer wants an agent to route to. All
other wiring (wait-state precision, threadId) stays untouched.
2026-04-19 01:19:23 -04:00
ZacharyZhang-NY f041336721 refactor: nuke dead exports, dead files, dead DB column, dead zero-value wrappers
Full-codebase ts-prune + manual audit sweep. Every change below is a pure
deletion (or a 2-line re-wire where a dead function was supposed to be
alive). No scope additions.

Dead wrappers / phantom features
  - shouldAutoApproveApproval: zero-value wrapper over shouldAutoApprove.
    Four call sites now import shouldAutoApprove from execution-policy
    directly.
  - evaluateRotation(session, connectorId, thresholdsOverride): the
    connectorId arg was never read (underscored), and thresholdsOverride
    was only exercised by the test suite — no production caller. Signature
    collapses to evaluateRotation(session). DEFAULT_THRESHOLDS /
    RotationThresholds exports and their tests go with it.
  - installLogRedactor: defensive console wrapper that was never wired
    from worker-entry. buildRedactor handles the known threat (env-value
    leaks); the pattern-based redactTokens stays for tests and future
    composition.

Dead files
  - resume-interrupted-tasks.ts: helper function never imported.
  - service-graph-orchestration-deps.ts: three factory closures
    superseded by the inline closures in create-orchestration-service.

Dead exports
  - listConnectorsByAuthKind (shared/connector-catalog)
  - getLocalePromptInstruction + whole localization.ts file
  - ImportantDocType
  - main-side ConnectorAuthKind (duplicated the shared one; not referenced)
  - CheckpointEngineService, RunHousekeepingService types
  - WorkspaceResolution type
  - cmdOrCtrlSymbol
  - AgentEventPush, CredentialPayload type aliases
  - parseApprovalPayloadJson, parseEnvValue, ProviderCard
  - getSOP — buildSOPSection reads the SOPS map directly; the individual
    getter had no callers. Deduped to a flat array.
  - renderWakeReason in wake-reasons/en.ts: byte-for-byte duplicate of the
    one in wake-reasons/index.ts.

Forgotten wiring (not cleanup, but adjacent)
  - registerAgentStandupRoute: the GET /api/agents/me/standup endpoint is
    advertised in every system prompt and wake-reason doc, but the route
    handler was never registered in api-server/index.ts. Agents have been
    hitting 404 since Pi Mono. Registered.

Dead DB column
  - v44 migration drops runs.context_snapshot_json. Column was added by
    v16-v25, never written or read, and v43's deliberate preservation
    was an interim measure. Same idempotent table-rebuild pattern as
    v41/v43.

Test tree
  - Phantom-feature threshold override tests removed (3). The rest of
    the rotation suite re-expressed against the real maxRuns=20 floor.

495/495 unit tests pass. typecheck + build clean.
2026-04-18 23:37:37 -04:00
ZacharyZhang-NY 88a1f8b334 refactor(connectors): drop dead CLI command field after pi-mono migration
The CLI-adapter era `connector.command` field became vestigial once every
provider dispatch went through pi-ai. The schema still required a non-empty
command, so onboarding saveConnector silently rejected every Pi Mono row
(which carries an empty default). Remove the field from the payload chain
(renderer → IPC schema → DB CRUD → run dispatch → worker job), narrow
RunJob.taskId to match EnqueueRunPayload, and drop the dead helper plus
its test. Schema column stays NOT NULL for DB compatibility; seedConnectors
writes a literal empty string so no migration is needed.
2026-04-18 23:09:59 -04:00
ZacharyZhang-NY d75bfd5851 refactor(model-pool): scope disabled-models by company; cascade on delete (v42)
The per-connector "enabled/disabled model" settings lived under a
global key in the settings key-value table
(`disabled_models:${connectorId}`). No company dimension. Deleting a
company left the pool choices behind, and creating a new company
silently inherited them — the visible symptom was "onboarding's team-
model configuration survives company deletion and pollutes the next
company's CEO model list."

Root fix, not a cleanup hook: the storage becomes a real relational
table with `(company_id, connector_id)` primary key and an
`ON DELETE CASCADE` FK from `companies`. Company deletion wipes pool
config by construction; no cleanup step in `deleteCompany()` can
drift.

Schema (migration v42):
- CREATE TABLE connector_model_pool_settings (company_id, connector_id,
  disabled_models_json, updated_at), PK (company_id, connector_id),
  FK companies(id) ON DELETE CASCADE.
- Drop every `settings.disabled_models:*` key — these were the bug's
  residue; there is no right company to attribute them to, so the
  migration clears them. Users re-select on next onboarding.

API surgery — every reader/writer now takes companyId:
- db.getDisabledModels(companyId, connectorId)
- db.setDisabledModels(companyId, connectorId, models)
- db.hasModelPoolSettings(companyId, connectorId)
- connector-model-policy: getConnectorModelPolicy(deps, companyId,
  connectorId); validateConnectorModelSelection(deps, companyId,
  connectorId, model)
- model-dispatch: listEnabledModels(companyId, deps);
  pickModelForAgent resolves companyId from agent.companyId
- orchestration heartbeat/task-runner/turn-context/run-dispatch pass
  the agent's or run's companyId through to the policy deps
- IPC schema: connectorDisabledModelsSchema gains companyId; new
  connectorModelPoolQuerySchema pairs companyId+connectorId for reads

Onboarding becomes commit-on-launch:
- useConnectorModelSelection no longer persists via IPC during
  onboarding. Toggles mutate local state; it exposes `poolSelections`
  to the wizard.
- bootstrapOnboardingSchema gains connectorPoolSelections. The
  onboarding service saves the company first, then persists each
  per-connector disabled list scoped to the new company id — CEO
  model validation runs against the company's freshly-persisted pool,
  not some stale global.
- IPC setConnectorDisabledModels now requires companyId; the only
  consumer was onboarding, which no longer calls it at all.
- Main-side listConnectorModels returns the raw provider catalog
  (no pool filtering), since that IPC is used from onboarding where
  no company exists yet; pool filtering happens at dispatch time.

saveConnector IPC drops the per-company pool validation — connector
config is a global hint, company-scoped enforcement belongs at run
dispatch where the company is known.

Tests: 603/603 green. Typecheck clean. Integration tests rewritten
to set the pool via connectorPoolSelections on bootstrap input, not
a pre-bootstrap global setDisabledModels call.
2026-04-18 22:14:09 -04:00
ZacharyZhang-NY 6abc1a823f refactor(inbox): derive from snapshot; delete getInbox IPC chain
Sibling refactor to the sidebar-badges cleanup. The inbox aggregates
five item categories (pending approvals, failed runs, budget warnings,
blocked/review tasks, unread company/incident/urgent messages) — every
row is already in `ProfileSnapshot`. The server-side `getInbox`
aggregation (180 lines of SQL + projection) was a redundant second
path.

New: `src/shared/inbox-items.ts::deriveInboxItems(snapshot, companyId)`
produces the same InboxItem[] shape with the same severity ordering
and 24h failed-run window. Pure function, no IPC, no state.

Deleted:
- database/company-crud.ts::getInbox (180-line aggregation)
- database-base.ts::getInbox pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- state/slices/company.ts: `inboxItems` field, `setInboxItems` action
- useDataLoader.ts: both getInbox call sites (initial snapshot load
  and section-change effect)
- useCompanyController.ts: inbox reset on company switch

OperationsSections now computes `inboxItems` via `useMemo` off the
current snapshot + company id. The snapshot is already company-scoped
at the main/database/snapshot.ts layer when `currentCompanyId` is
non-null; the derivation re-filters defensively so the function works
correctly even for unscoped snapshots.

Tests: 603/603 green. No new tests added — the derivation is a pure
function whose shape is exercised end-to-end by the existing Inbox
component integration.
2026-04-18 21:42:58 -04:00
ZacharyZhang-NY 487f2456ff refactor(badges): derive from snapshot; delete getSidebarBadges IPC chain
Badges were fetched via a dedicated IPC that aggregated counts on the
server, while the same data lived in ProfileSnapshot on the client.
Two paths to the same answer drift whenever one side mutates between
refreshes — the visible symptom being "badge = 1 unread" with an empty
inbox after a message gets marked read in another screen.

Delete the whole server path. Sidebar badges are now derived on the
client from the snapshot via `deriveSidebarBadges(snapshot, companyId)`
in `src/shared/sidebar-badges.ts`. One source of truth; drift is
physically impossible.

Deletions:
- database/company-settings.ts::getSidebarBadges (120-line SQL aggregation)
- database-base.ts::getSidebarBadges pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- shared/contracts/schemas-badges.ts::sidebarBadgesSchema Zod schema
- renderer/state/slices/company.ts: `badges` field, `setBadges`,
  `refreshBadges`
- useDataLoader: both getSidebarBadges call sites
- useCompanyController: badge reset on company switch
- useDesktopEvents: refreshBadges call on granular patches
- tests/unit/store.test.ts: refreshBadges test (deleted code)

The snapshot's agentMessages already carries board-reader read state
via boardReadReceiptMap (`snapshot.ts:39-47` has always LEFT JOIN'd
agent_message_reads with agent_id = BOARD_READER_ID). So derivation
needs no schema change — the infrastructure was in place, just unused.

fix(communication/MessageList): render orphaned-reply messages at the
top level

The thread-flattening used `filteredMessages.filter(m => !m.parentMessageId)`
to pick top-level messages. When an agent replies to a company/incident
post with a DM (the CEO prompt explicitly instructs this in
wake-reasons/en.ts:119), the DM lands in the direct channel with
parentMessageId pointing to a message that lives in another channel.
That parent is not in `filteredMessages` for the direct channel, so the
reply had no parent to nest under AND was excluded from top-level. The
channel rendered as a blank pane even though exactly one message
matched — the "clicked Dr. X ↔ Dr. Y, got empty" symptom.

Fix: a message is top-level when it either has no parent OR its parent
is outside the current filtered view. Replies with in-view parents
still nest normally.
2026-04-18 21:38:34 -04:00
ZacharyZhang-NY c83d670050 ui(onboarding): flat tab navigation + fix vertical-stacked button text
Onboarding was a 7-step linear wizard, same information hierarchy as
Settings but behaving like a modal. Replace with the SettingsShell tab
pattern: one row of six tabs (nodecheck / providers / company /
modelpool / executionmode / mission), free navigation, per-tab
completion ticks derived from the wizard's own validation rules.

Sub-fixes on the same pass:
- ActionButton: add whitespace-nowrap + shrink-0 so "选择" never
  stacks vertically on narrow containers. Global — every caller gets
  the correct behavior
- CompanyStep: CEO Runtime Provider/Model go vertical (space-y-3
  instead of grid-cols-2) so long names like "Fireworks AI" /
  "Kimi K2.5 Turbo Preview 0911" stay readable; workspace-path Input
  wrapped in min-w-0 flex-1 container so it yields, not the button
- MarkdownBody: render react-markdown in chat bubbles (was plain text)

i18n: added onboarding.stepsAria for the tablist label.
2026-04-18 21:38:11 -04:00
ZacharyZhang-NY c71d033b0c refactor: prune dead IPC surface and orphaned modules
Drop code that nothing consumed. Verified by full renderer+shared grep:
every removed channel had zero callers via window.agentCompany.* and
zero references from desktop-api types. No backend internal use either —
the database/API-server layers keep their methods, only the IPC exposure
goes.

Removed IPC handlers (26 total):

  Thread CRUD surface (12) — writes happen inside the backend
    createThread, listThreads, addThreadEvent,
    setThreadWaitState, resolveThreadWaitState,
    addParticipant, removeParticipant, listParticipants,
    subscribeThread, unsubscribeThread,
    listSubscriptions, listSubscribedThreads
    (kept reads: getThread, listThreadEvents, listThreadWaitStates)

  Half-implemented features (13)
    saveSecret, listComments, getAgentMetrics, getApiPort,
    listWakeupRequests, listAgentMessages, autoAssignTask,
    listRunEvents, recoveryDismiss,
    getHeartbeatPolicy, setHeartbeatPolicy,
    getRuntimeState, setRuntimeState

  Whole vertical slice: openConnectorAuthTerminal
    IPC handler + channel + preload + DesktopApi type +
    connector-auth-service function + useConnectorCallbacks
    handleLoginConnector + OnboardingContext.onLoginConnector +
    OnboardingWizard wiring. Pi-mono has no CLI auth flow;
    the no-op that "returned false for legacy UI compatibility"
    had no legacy UI left.

Orphaned modules (zero importers in repo):
  src/main/runtime-env.ts
  src/main/database-stale-recovery.ts

Dead schema files (only referenced by their own barrel re-export):
  schemas-participants, schemas-subscriptions, schemas-heartbeat,
  schemas-run-events, schemas-runtime-state, schemas-wakeup

Renderer leftovers:
  .activity-row-enter CSS + its two unreferenced keyframes
  5 orphan i18n keys (app.console, app.refresh, app.expand,
  activity.timeline, auto.trigger) left over from earlier feature
  deletions

Also update two unit tests that mocked openConnectorAuthTerminal in
the RegisterCoreHandlersDependencies pick.
2026-04-17 22:38:10 -04:00
ZacharyZhang-NY b68307b24f refactor(sidebar): drop company name from delete-confirm prompt
"删除 OrbitOps AI?" was being truncated to "删除O..." inside the
~100px inline confirm row, because the row has to share width with
the 是 / 否 buttons. The name-interpolation was the wrong half of
the message to keep: the user has just clicked the trash icon on a
specific row, so **which** company is about to be deleted is
already anchored by the row they are looking at. The bare
confirmation "删除?" / "Delete?" fits comfortably and loses zero
information.

- zh key: "删除{name}?" → "删除?"
- en key: "Delete {name}?" → "Delete?"
- Removed the `.replace("{name}", company.name)` call and the
  fallback string that also interpolated `company.name`.

No visual change to the 是 / 否 buttons; the confirm still blocks
the row, still requires an explicit click. The only thing gone is
the truncated company-name echo.
2026-04-17 21:32:59 -04:00
ZacharyZhang-NY 68f2725918 refactor(sidebar): delete switcher caret + palette kbd label + dropdown check column
Three subtractions on the 220px-wide sidebar top bar, where the
company name was being truncated to "OrbitOps ..." inside the button
AND to "Orbit..." inside the open dropdown. Fixing the truncation by
widening the sidebar would be adding space; Jony+Jobs read this as a
signal that too many things were competing for the same width.

Removed:

1. `CaretUpDown` icon at the right edge of the switcher button
   (14px + 8px gap = ~22px). A clickable company name is obviously a
   picker — the same pattern the Mac menu bar and Slack workspace
   switcher use without a perpetual caret attached. The button's
   hover state + the dropdown itself carry the affordance.

2. `<kbd>⌘K</kbd>` label inside the command-palette button beside
   the switcher (~30px). The palette already opens from the global
   ⌘K shortcut, which persists unchanged; the button's `title`
   attribute keeps the shortcut discoverable on hover. Collapsed-
   sidebar mode already rendered the palette button icon-only —
   expanded mode now matches that baseline instead of duplicating
   the accelerator hint.

3. The 16px `w-4` check column inside each dropdown row. The active
   company is already marked by `font-semibold text-foreground`
   against the other rows' `text-muted-foreground` — double-marking
   "this is current" with a ✓ AND bold + colour was the same
   information twice, and it stole width from names that were
   themselves being truncated.

Net effect: switcher button gains ~52px of text width; dropdown rows
gain ~22px. Companies with names like "OrbitOps Solutions" that
previously truncated now render in full at the default sidebar width.

Typecheck + lint pass. Unused `CaretUpDown` / `Check` icon imports
dropped.
2026-04-17 21:27:17 -04:00
ZacharyZhang-NY 7f4acd0fb7 refactor(messages): delete triple department signal + decorative read-state indicator
Two subtractions in the message bubble header row:

1. Department chip next to each agent name. Department was already
   communicated by (a) the avatar tile colour (`bg-{color}18`, tinted
   by department via DEPARTMENT_COLORS) and (b) the sender name's
   text colour (`color: senderColor = deptColor`). The explicit
   uppercase chip ("ENGINEERING", "PRODUCT", …) was the third signal
   in the same row and the loudest — hardest-to-read typography
   (10px uppercase tracking) re-stating what colour already says.
   Deleted; colour continues to carry the semantics.

2. `CheckCircle` as the read-state marker. It only appeared
   on hover at 60% opacity — invisible most of the time and purely
   decorative when visible. The absence of the unread dot already
   communicates "read"; a second glyph just to mark the negative
   case added nothing. Deleted along with the now-unused CheckCircle
   import.

Typecheck + lint pass.
2026-04-17 21:22:10 -04:00
ZacharyZhang-NY 7c4d7a4a5e refactor(hiring): delete three redundant sections (pending-panel + summary cards + department headcount)
HiringPipeline carried three panels that restated information already
shown — one with more emphasis — on the Team page or in the stage
filter directly above them:

1. "Pending approval" mini-Panel (lines 121-187): listed the same
   candidates that the user reaches by clicking the `pending_approval`
   chip in the stage filter at the top of the page. The mini-Panel
   also carried its own Approve / Reject buttons, but those approvals
   are resolvable from Approvals section too. Doubling the approval
   path increases the chance of racing clicks between the two
   surfaces. Deleted.

2. 4-card summary row at the bottom (lines 246-267): Total agents /
   Pending hires / Departments / Monthly payroll. The first two are
   rendered on the Team page by TeamHeaderSummary (commit 36bed1b).
   Payroll can return via a Team-page dedicated surface when there
   is a business reason; until then it is clutter. Deleted.

3. "Department headcount" Panel (lines 269-293): grid of active+
   pending count per department. OrgChart already shows this through
   the department colour on each agent card, and TeamHeaderSummary
   covers the aggregate. Two places describing the same fact is one
   too many. Deleted.

Also:

- `onRefresh` prop removed from HiringPipeline + OrganizationSections
  caller — only the (now-deleted) approve/reject buttons used it.
- `Card`, `UserPlus`, `Check`, `unwrap` imports dropped because the
  deletions made them unused.
- Unused `i` index in Dashboard.tsx activeWork map callback cleaned
  up (left over from the previous commit that removed the
  arbitrary animation gate).

File size: HiringPipeline.tsx 296 LOC → 175 LOC (net −121).

Typecheck + lint pass.
2026-04-17 21:21:05 -04:00
ZacharyZhang-NY 8b6826f6d8 refactor(inbox): delete uppercase type tag from each row
Each inbox row previously carried a `text-[9px] uppercase tracking-
[0.1em]` pill next to the title that re-stated the item's type
(APPROVAL / REVIEW / FAILED RUN / …). The row already communicates
type through three stronger channels:

1. The coloured icon tile on the left (`config.icon` + `config.bg`)
2. The filter chip above the list when a type filter is active
3. The border accent colour tied to severity

The 9px pill added another restatement in the smallest, hardest-to-
read typographic register on the page. Deleted. The title row now
just shows the item title; the icon carries the category semantics.

No copy or behaviour change beyond the visual subtraction.
2026-04-17 21:19:03 -04:00
ZacharyZhang-NY c7057d0fd2 refactor(dashboard): delete redundant company-status line + arbitrary animation cap
Two subtractions surfaced by the fresh Jony+Jobs pass on the Dashboard:

- The "Company status line" at the top of the dashboard showed three
  counters: `{activeAgents} agents active · {activeTasks} tasks in
  progress · {completed} completed`. The 4-card MetricsGrid directly
  below it already rendered the same agent count and active-tasks
  count as its primary tiles. Seeing the same number in vertical
  succession is the cheapest way to tell users "we weren't sure
  what to show." Deleted the whole status-line block + the companyName
  prop + the now-unused caller wiring in OperationsSections.

- The Active Work list rendered a `animate-ping` halo on the first
  two running runs (`i < 2`) and skipped it for the rest. Arbitrary —
  either the ping is a meaningful "I'm active" signal for every
  running run, or it is decoration. Dropped the ping entirely; the
  solid green dot on the robot icon is already enough to say
  "running," and losing the double-layered animation lets the
  dashboard feel calm instead of twitchy.

Dead i18n keys removed alongside (zh + en):
  dash.agentActive / dash.agentsActive / dash.taskInProgress /
  dash.tasksInProgress / dash.completedCount.

Typecheck clean.
2026-04-17 21:18:42 -04:00
ZacharyZhang-NY 50685b952c refactor(agent-form): add tab navigation inside Edit modal; pair short fields in 2 columns; delete dead hidden input
The Edit modal was still a vertical scroll of 5 stacked sections (12+
form fields + an embedded Heartbeat card + action bar) even after C5
named the sections. A modal is a transient affordance — making the
user scroll inside it was the wrong design. Same IA move we made for
the Settings page one round ago: add tabs.

Tabs inside the modal:

  Identity | Runtime | Budget & scope | Behavior

Each tab swaps the pane. Tab label carries the section context, so
every section body lost its SectionHeader wrapper — the nested
"card-in-a-card" look around HeartbeatSection is gone; Behavior tab
just renders the heartbeat controls directly.

Field-level polish bundled in the same pass because it hit the same
render paths:

- Identity tab: Name + Role paired in a 2-column grid; Department +
  Reports To paired. Title spans full width because its values are
  often longer. The modal no longer grows tall just to fit two short
  inputs side by side.
- Runtime tab: Status stands alone at the top (high-frequency
  control — pause/resume). Connector + Model paired; Thinking Effort
  + Workspace paired. Each paired row keeps its inline hint (connector
  issue / effort-not-supported) tied to the field it belongs to,
  instead of appearing as loose paragraphs between fields.
- Budget tab: Budget input + BudgetBar + burn line cluster into one
  grouped column so typing a new cap updates the visual context in
  place.
- Behavior tab: HeartbeatSection rendered at the tab root — no
  SectionHeader wrapper around it. The card's "Continuous Execution"
  title + Wake-now button now read as the primary content of the
  tab, not as something buried beneath a header.

Also removed:
- `<input type="hidden" data-agent-id={agent.id} />` in RuntimeSection
  — dead code, zero semantic purpose, leftover from an earlier
  experiment. Caught during the audit.
- `SectionHeader.tsx` — no more callers after sections stopped
  wrapping themselves in it. File deleted.

Added i18n keys (zh + en):
  agent.form.tabsAria.

Tab-label copy reuses the existing `agent.form.sectionIdentity`,
`sectionRuntime`, `sectionConstraints`, `sectionBehavior` keys —
same strings that the removed SectionHeader rendered, now carried
by the tab labels.

Covers EJ1 / EJ2 / EJ3 / EJ4 / EJ5 / ES1 / ES3 / ES4 from the
second-round Edit-modal audit.
2026-04-17 21:14:00 -04:00
ZacharyZhang-NY 1933fd6192 refactor(settings): relocate profile-path + secret-backend diagnostics into Data tab
Profile path and secret backend are **read-only diagnostic facts** —
they tell you which directory the profile lives in and which keystore
ciphertexts are stored against. They are not preferences; they do not
belong in the General tab alongside the theme / language / auto-update
controls.

Moved both into a new "Profile" group inside the Data tab, rendered
via a small `ReadOnlyRow` helper so both pieces of information share
consistent bordered-card styling and get a clear label/value split.
General tab now contains only the three actual preferences (theme,
language, updater) + the inline updater hint.

The previous General rendering dropped profile path as raw mono text
under the updater checkbox — visually tangled with the preference
control. With diagnostics in Data and preferences in General, each
tab reads with a single purpose.

No new i18n keys required — reused `exec.profile` / `exec.profilePath`
/ `exec.secretBackend`, which were already translated in zh + en.

Follow-up polish; no behavioral change. Typecheck + lint pass.
2026-04-17 21:08:49 -04:00
ZacharyZhang-NY 03bcc8a8f0 feat(settings): add top-row tab navigation; fold Providers + mode + secrets into one IA
The settings page was a single vertical scroll containing seven
unrelated concerns. Users had to scroll past theme preferences to
reach Providers — which is the most-touched surface — and mode +
secrets + shortcuts lived below that. There was no signal about
where anything was.

Introduced `SettingsShell` with a horizontal tab row along the top:

  General | Execution mode | Providers | Shortcuts | Data | Secrets

Each tab swaps the content pane. Active tab is visually anchored
with a 2px primary underline + foreground text; inactive tabs stay
muted with a hover promotion. All tabs are real `role="tab"` +
`aria-selected` so screen readers announce the current pane; the
nav has `aria-label="Settings categories"`.

Tab state is local — settings are ephemeral navigation, no reason
to persist across company switches or app restarts. Default is
"general" because that's where first-time users land.

Refactor consequences:

- `OperationsSections.tsx` now renders `<SettingsShell />` + the
  OAuth prompt modal portal; the old double-Panel wrapper with
  "Profile and release" + "Providers" titles is gone. The "Profile
  and release" label always fought with what was actually inside
  the pane — Jobs audit item S-S2 addressed.
- Providers moved from a separate sibling Panel into the main IA.
  Previously Providers came AFTER a 7-section scroll; now it is a
  peer of General / Mode / etc., reachable in one click.
- Deleted the now-unused `SettingsPane.tsx` orchestrator. Its
  single-column rendering survived only as a composition step that
  SettingsShell replaces.

Added i18n keys (zh + en):
  settings.tabs.general / mode / providers / shortcuts / data /
  secrets; settings.tabsAria.

Covers J-S2 / J-S7 / S-S5 / S-S7 from the settings audit.
2026-04-17 21:07:52 -04:00
ZacharyZhang-NY 16e45b6a8d refactor(settings): split 73-line god file into 5 sections + auto-save preferences + guard Restore
The previous SettingsPane.tsx hid 8 unrelated concerns behind 73 "lines"
of JSX where each line ran 400-2000 chars. Reading the file was
infeasible; editing it meant scrolling horizontally through a single
statement spanning every button, modal, and table row.

Split by concern into `settings-sections/`:

  SettingsPane.tsx           62 LOC  orchestrator
  GeneralSection.tsx         94 LOC  theme / language / auto-update
  ExecutionModeSection.tsx  105 LOC  AI CEO ↔ AI Co-Founder toggle
  ShortcutsSection.tsx       40 LOC  keyboard reference (read-only)
  DataSection.tsx           123 LOC  update check / backup / restore /
                                     secret backend
  SecretsSection.tsx        102 LOC  per-company secret bindings

Every file now reads top-to-bottom with normal indentation.

Behavioral changes bundled (they belong with the restructure, not a
separate pass):

- Preferences save model unified: theme already auto-saved on change;
  language and auto-update checkbox now do the same. The global
  "Save settings" button is gone — there is no longer a state where
  a toggled language sits unpersisted waiting for a second click.
- Execution mode card gained a subheader line that names the current
  company ("Applies to current company: X"). Multi-company users can
  no longer confuse which company they just flipped.
- Auto-updater checkbox gained an inline hint: "Check for application
  updates at launch" — the previous naked checkbox never explained
  what happens when it is off.
- Restore profile moved into DataSection's backup/restore group,
  given `tone="danger"`, and gated behind a `window.confirm` with
  copy warning about running work being lost. Backup stays neutral.
  The previous layout put Restore in the same 2-col grid as Save +
  Check Updates + Backup, making a profile-replacing operation look
  as routine as saving a theme preference.
- Secret backend display promoted from a naked text line to a
  bordered card so it matches the surrounding section rhythm.

Added i18n keys (zh + en):
  settings.updaterHint / settings.modeScope / settings.backupRestore /
  settings.restoreConfirm / settings.restoreHint.

Covers J-S1 / J-S6 / S-S1 / S-S2 / S-S3 / S-S4 from the settings audit.
Remaining work (tab sidebar, Providers IA reordering, copy sweep)
lands in follow-up commits.
2026-04-17 21:06:07 -04:00
ZacharyZhang-NY fd6471988a refactor(app): remove Console feature entirely
Per user request: the "显示控制台 / Show console" button in the top-right
header and the bottom drawer it opened are no longer needed — the Runs
section already renders the same log tail in-page via RunDetail, so the
drawer was a duplicate surface. Deleted the entire feature instead of
leaving a hidden toggle that attracts bugs.

Removed files:
- src/renderer/views/ConsoleView.tsx (126 LOC)

Removed renderer integration points:
- App.tsx: header "Show/Hide console" button, ConsoleView render,
  setConsoleOpen(true) call after startRun, TerminalWindow icon import
- views/sections/ExecutionSections.tsx: onOpenConsole callback,
  setConsoleOpen subscription
- components/runs/RunCenter.tsx: onOpenConsole prop (removed from
  forwarding chain)
- components/runs/RunDetail.tsx: onOpenConsole prop + the Lightning
  "Open console" ActionButton. The cancel-run button remains as the
  sole Panel action now that the console entry point is gone.
- hooks/useUIController.ts: consoleOpen / setConsoleOpen subscription
- hooks/useDesktopEvents.ts: "toggle-console" shortcut dispatcher
- state/slices/navigation.ts: consoleOpen field, setConsoleOpen,
  toggleConsole actions, and the two auto-open-on-navigate triggers
  that used to flip the drawer when jumping to a run

Removed main-process integration:
- main/desktop-shell-menu.ts: "Toggle Console" menu item + CmdOrCtrl+`
  accelerator
- main/desktop-shell-window.ts: onShortcut union type narrowed
- shared/types/events.ts: ShortcutEvent action union narrowed

Removed i18n keys (zh + en):
- app.hideConsole / app.showConsole
- console.title / console.noRuns / console.close
- common.openLog / common.noRunSelected / common.waitingForLog
- runs.openConsole

Log capture itself (logBufferRef in useCompanyController + run-log
events) is unchanged — the Runs section still displays it. Nothing
was moved to a "disabled but present" state; the feature is out of
the tree entirely. Typecheck and lint pass.
2026-04-17 20:32:34 -04:00
ZacharyZhang-NY dfebced29f refactor(agent-form): hide Skills section from Edit modal
Per user request: remove the Skills toggle list from the agent Edit
modal. Skills stay accessible through their own dedicated Skills
section in the command palette (paletteOnlyNavDefs in useUIController),
so nothing is actually gated away — the Edit modal just stops
double-rendering the control.

Also removed the unused SkillsSection.tsx file since nothing else
references it (added only in C5 / commit 9981ee3, never used outside
EditFormBody). If skill assignment needs to come back into the
modal later, git history has the exact component intact.
2026-04-17 20:32:18 -04:00
ZacharyZhang-NY 3608578ba3 refactor(communication): localize ChannelHeader strings; bump tiny chips from 9px to 10px
Stragglers from the Communication audit that did not fit into C10's
FilterBar scope:

- ChannelHeader carried two hardcoded English strings: the sidebar
  reveal button's aria-label ("Show sidebar") and the "Open task"
  link that appears on task-discussion channels. Both now go through
  i18n — added `comm.showSidebar` ("显示侧边栏" / "Show sidebar") and
  reused the existing `comm.openTask` key. Users running in Chinese
  now get a fully Chinese header.

- MessageBubble had two 9px uppercase chips (department badge and
  priority badge). 9px with tracking is unreadable at standard display
  density; bumped both to 10px and loosened the tracking slightly so
  the letters don't collide. Same typography decision we made for
  the OrgChart connector label in C3.

Covers the remaining Communication audit items. J-C1 (resizable
sidebar) and J-C6 (resizable ThreadPanel) are feature additions and
stay out of scope per the audit's "bug fix only" contract.
2026-04-17 20:16:43 -04:00
ZacharyZhang-NY e89fe043f7 refactor(providers): split displayHint from status color + normalize type scale
Two providers-panel audit items landed together because they both
come down to "the eye should have to track one piece of status info
in one place":

- ProviderRow displayHint (e.g. "sk-ant-...abcd", "claude.pro@gmail.com",
  "AWS profile: prod") was color-coded by credential status. That made
  the hint itself read as green/amber/red — e.g. an active connection
  would render its hint in success green, which looked like the hint
  was announcing its own success. The dot on the left already carries
  the status signal; the hint should just identify which credential we
  are looking at.

  Split the second line so the hint is always muted-foreground. When
  status is anything other than `active` or `not_configured`, we
  append a colored status label after a separator ("· Expired") so
  the hint stays readable while the anomaly still surfaces.

- Type scale cleanup: the group header and count used `text-[10px]
  tracking-[0.12em]` which was the smallest text on the page. Bumped
  to `text-[11px] tracking-[0.08em]` to match the section-header
  rhythm established in the agent Edit form (commit 9981ee3), and
  added `tabular-nums` to the count so alignment is stable across
  groups of different sizes. Auth-kind chip (OAUTH / API KEY / AZURE /
  AWS / GCP) likewise promoted from 10px to 11px with the same tracking.

No behavioral change — these are readability fixes only. Covers
J-P2 / J-P3 / J-P5.
2026-04-17 20:13:56 -04:00
ZacharyZhang-NY bbfb02fcdd refactor(console): boost log contrast + dedupe empty state
The Console panel (the bottom drawer opened with `显示控制台` / `Show
console`) had two issues the audit flagged as "not readable by a
normal user":

1. Log output was rendered in `text-muted-foreground` — roughly 45%
   contrast on dark backgrounds. Terminal output needs high contrast
   because users scan for error keywords at a glance. Boosted to
   `text-foreground/90`, which tracks the theme's foreground color so
   logs read clearly in both dark and light themes. Run meta values
   (Started / Finished / Cost) also promoted from muted to foreground;
   the field labels stay muted so the value is the emphasized element.

2. Empty state "No run selected" rendered TWICE — once in the left
   meta pane, once in the log pane. Restructured: when no run is
   selected, hide both panes and show a single centered message in
   the full-width drawer area. Faster to parse, less visual noise.

Covers J-Co2 / S-Co1. Other Console items (live log filtering, clear
button, cost emphasis near budget cap) are deferred — they are
feature additions, not bug fixes, and belong in a separate commit
with UX sign-off.
2026-04-17 20:13:07 -04:00
ZacharyZhang-NY 1f3484ac19 refactor(communication): polish FilterBar + thread time + composer shortcut
Communication page audit items from the second-round pass landed as
one commit because they sit on overlapping render paths:

- FilterBar had two native <select> elements (channel type / priority)
  using raw option tags. After C9 normalized the Select primitive with
  an explicit CaretDown, these two were the last stragglers. Migrated
  to Select compact. Chevron affordance now matches the rest of the app.
- FilterBar search input carried a `focusRingColor: "var(--primary)"`
  inline style that is NOT a valid CSS property; it had zero rendering
  effect. Replaced with Tailwind `focus:border-[color:var(--primary)]
  focus:ring-1 focus:ring-[color:var(--primary)]` which actually draws
  the focus ring the author intended.
- FilterBar primary button + unread-only pill used inline
  `style={{ backgroundColor: "var(--primary)" }}` to work around the
  (formerly missing) Tailwind color token. `bg-primary` is now defined
  via the theme tokens — switched both to Tailwind utilities so we
  don't mix CSS-in-JS with class-based styling in a single component.
- ChannelItem unread badge: 9px tabular-nums. 9px was unreadable on
  1x displays; bumped to 10px and added `tabular-nums` so multi-digit
  counts (10, 99, 99+) don't shift alignment.
- ThreadPanel event timeline time was formatted with
  `toLocaleTimeString([], …)` which ignores the app's zh/en locale
  setting. Switched to the shared `formatMessageTime` helper so the
  thread respects locale and also handles yesterday/earlier dates.
- MessageComposer shortcut hint was a hardcoded English string
  "Press {cmd}+Enter to send". Replaced with the existing
  `comm.sendShortcut` locale key ("按 {0}+Enter 发送" / "Press {0}+
  Enter to send") + cmdOrCtrlLabel() interpolation.

Covers J-C2 / J-C3 / J-C4 / J-C5 / J-C7 / J-C8 from the Communication
audit. The remaining J-C1 (hardcoded 260px sidebar) and J-C6 (380px
thread panel) are intentional — resizable panels would be a separate
UX feature, not a bug fix.
2026-04-17 20:12:19 -04:00
ZacharyZhang-NY e87170f105 refactor(ui): render explicit CaretDown on Select; normalize affordance across platforms
Native <select> chrome varies: macOS renders a dual-arrow indicator,
Windows and Linux render a single caret, and on dark themes the
native caret can drop to near-invisible contrast. The agent Edit
modal (image 5 in the audit) had five side-by-side Selects where the
indicator read as a subtle decoration, making the field look closer
to a filled-in text input than a pick-from-list.

Switched Select to:
- `appearance-none` to strip native chrome
- Phosphor CaretDown rendered absolutely on the right, bold weight,
  muted-foreground so it reads against both card and background
- Padding-right increased to reserve space so long option labels
  do not overlap the caret
- Opacity drop when disabled so the affordance tracks the field state

Scope: only the shared Select primitive; every caller inherits the
fix automatically. No API change.

Covers J10.
2026-04-17 19:58:42 -04:00
ZacharyZhang-NY 36bed1b0f1 feat(team): add TeamHeaderSummary strip + delete unreachable orgchart section
Two S-level audit items landed together because they are the same
"first-second orientation" concern on the team page:

S1 — Empty OrgChart was the first thing the board saw; the canvas
     carried no summary data. Now a compact summary strip sits above
     the chart showing Members / Running / Attention / Pending hires /
     This-month budget (spent/cap/%). All five numbers computed from
     the existing snapshot — no extra IPC.
S9 — Activity and budget context previously required visiting three
     separate pages. The summary strip collapses the minimum-useful
     subset into a single glance; the full drill-down paths (Work,
     Approvals, Costs) remain one click away.

S10 — The `orgchart` SectionId was a dead rendering branch:
       - Not in the sidebar (useUIController sidebarGroups)
       - Not in the command palette (paletteOnlyNavDefs)
       - Not reachable from any navigateTo() call in renderer code
     The only callers were an automation post-hire notification and a
     unit test — both pointed at "orgchart" as a navigation target.
     Routed both to "agents" instead (which renders the OrgChart at
     the top anyway, plus the new summary strip and the roster), so
     clicking the "Agent Hired" notification now lands on the team
     page with the new hire highlighted.

Removed:
- `orgchart` from SectionId enum (src/shared/types/enums.ts)
- `section === "orgchart"` render branch (OrganizationSections.tsx)
- `nav.orgchart` i18n key (zh + en)

Added:
- src/renderer/components/organization/TeamHeaderSummary.tsx
- team.summary.* i18n keys (zh + en)

Covers S1 / S9 / S10.
2026-04-17 19:57:58 -04:00
ZacharyZhang-NY ad00a91754 chore(copy): humanize team-page labels; drop dead org.fit key
Jobs audit item S11: "智能体名册" (Intelligent-agent roster) reads as
machine-generated in a UI whose left navigation already uses the human
word "团队" (Team). The collision between the technical term and the
domain term made the team page feel less real than it is — it is a page
about running an AI company whose members happen to be agents, so the
label should match the nav.

- `org.agentRoster`            "智能体名册"          → "团队成员"
- `org.agentRoster` (en)       "Agent roster"        → "Team members"
- `org.noAgentsYet`            "暂无已招聘的智能体"    → "还没有团队成员"
- `org.noAgentsYet` (en)       "No agents hired yet" → "No team members yet"
- `org.noAgentsDetail` (en)    "Hired agents..."     → "Hired members..."
- `org.hireRequestNote`        Reworded to use "新成员" + tighter flow
- `agent.form.pendingReadOnly` "编辑智能体资料"       → "编辑成员资料"

Technical contexts where "智能体" is the subject matter (skills injection
hint, workload charts, run tables, etc.) are left untouched — the word
is the product's central noun and replacing it everywhere would harm
clarity for users reading technical copy.

Also dropped the dead `org.fit` / "Fit" key — replaced by `org.fitChart`
in commit 50a7196 when the Fit button became an icon. Zero orphaned
callers (verified by grep).
2026-04-17 19:55:27 -04:00
ZacharyZhang-NY dc33131553 refactor(selection): split selectedAgentId into focus vs edit-modal states; improve Edit modal title
Before this commit one piece of state (`selectedAgentId`) did three
jobs: roster highlight, navigation destination, and Edit-modal open
flag. The three jobs had incompatible lifecycles — highlight should
persist across modal close, navigation should land on a screen
without opening a modal, and the modal itself only wants to open on
explicit intent. The conflation was the root cause of the "Edit: CEO
auto-opens on first Team view" bug (commit 41132e2 patched the most
visible symptom by not pre-selecting the lead agent, but the coupling
remained).

Split into two slice fields:

- `selectedAgentId` — which agent is currently highlighted in the
  roster / OrgChart. Survives modal close. Set by: row click, card
  click, navigation from Inbox / Hiring (all of which pass through
  `openAgentEditor` which sets both so the highlight follows the last
  interaction).
- `editingAgentId` — which agent's Edit modal is open, or
  NEW_ENTITY_ID for the "Request new hire" modal. Only set by
  explicit user action; cleared on modal close. Never set by
  navigation or onboarding — the pre-select pathway cannot
  accidentally open a modal again.

Plumbing:

- `selection.ts`: new `editingAgentId` + `setEditingAgentId`, plus
  clear on `clearCompanyScopedSelections`.
- `useCompanyData.ts`: derive `editingAgent` record from the id.
- `useCompanyController.ts`: forward `editingAgent` on the section
  context.
- `OrganizationSections.tsx`: `agents` section modals read
  `editingAgentId` / `editingAgent`. "Request new hire" button uses
  `setEditingAgentId(NEW_ENTITY_ID)`. Single-click handlers go
  through `openAgentEditor(id)` which updates both states.

Also improved Edit modal title (was "Edit: CEO", a machined English
prefix on a Chinese UI with zero context about the role):

- zh: "编辑 {name}({role})"
- en: "Edit {name} ({role})"

Renders as "编辑 CEO(首席执行官)" in the screenshot example.

Covers J9 / S2.
2026-04-17 19:54:16 -04:00
ZacharyZhang-NY 9981ee342b refactor(agent-form): split EditFormBody into 5 semantic sections + add Cancel/dirty-state + always-visible Thinking Effort + binary OrgChart hierarchy
Previously the agent Edit modal was a 351-LOC wall of 17 stacked form
fields with no visual grouping, mixed header styles between the
skills heading and the field labels, a Save button that was always
enabled, no Cancel affordance, and a Thinking Effort field that
silently disappeared when the selected connector did not support it.

This commit restructures the edit surface into five named sections
— Identity, Runtime, Budget & scope, Behavior, Skills — each with a
consistent SectionHeader (11px uppercase muted, border-top divider).
Every section lives in its own ≤150-LOC file under
agent-form-fields/edit-sections/ so nothing stays god-sized:

  EditFormBody.tsx          199 LOC (orchestrator: isDirty, handlers)
  IdentitySection.tsx        74 LOC
  RuntimeSection.tsx        114 LOC
  ConstraintsSection.tsx     58 LOC
  SkillsSection.tsx          36 LOC
  EditActionBar.tsx          87 LOC
  SectionHeader.tsx          18 LOC

Behavioral changes:

- Save is disabled until the form is dirty (trimmed-string + numeric
  budget comparison against the persisted agent record). The board
  sees whether changes are captured without clicking Save to find out.
- Cancel button added, wired through AgentForm → AgentEditor →
  OrganizationSections, which clears selectedAgentId (same as the
  modal's close X — a proper bottom action avoids users reaching for
  the corner).
- Thinking Effort is always shown. When the connector provider does
  not accept the hint the Select is disabled with an inline note,
  so the option is discoverable instead of silently hidden.
- Budget shows a live BudgetBar + "Spent $X of $Y" line when the
  budget or spend is non-zero. The raw number input on its own left
  burn state invisible.
- Capabilities placeholder replaced with a concrete example
  ("writes React components, reviews PRs, ...") — the previous
  "What this agent does" was a circular prompt.
- OrgChart role bar went from three tiers (CEO 4px primary /
  manager 3px warning / IC 2px muted 30%) to binary (root 4px
  primary / everyone else 2px border). Orange-warning for managers
  read as an alert, not a rank; managerial position is already
  encoded in the tree's topology.

Also added i18n keys (zh + en):
  agent.form.sectionIdentity / sectionRuntime / sectionConstraints /
  sectionBehavior / effortNotSupported / budgetBurn; updated
  capabilitiesPlaceholder copy.

Covers J2 / J7 / J8 / S5 / S6 / S7 / S8.
2026-04-17 19:50:53 -04:00
ZacharyZhang-NY 62d0964af3 feat(org): add OrgChart status legend + CEO delete/terminate guards
Two audit items addressed together because they share the same org-page
render surface and both enforce "read the screen correctly before acting":

1) Status dot colors in OrgChart were undocumented — users saw five
   distinct colors (running/active/idle/paused/error/terminated/
   pending_approval) with no key. Added a compact top-left legend that
   only lists the statuses actually present in the current org, so a
   healthy all-running company shows two rows, not seven. Label copy
   reuses existing agent.form.status* keys (zh/en).

2) The agent Edit modal offered Delete and Status=terminated on every
   agent, including the root (no reportsTo) — the CEO. Deleting or
   terminating a root orphans every direct report. Added `isRootAgent`
   check in EditFormBody:
   - Delete button: disabled, with `rootDeleteBlocked` inline note
     telling the user to reassign reports first.
   - Status select: `terminated` option hidden; `rootTerminateBlocked`
     note explains why.

Scope note: this is UI-level guardrail only. The deleteAgent IPC /
API route is unchanged — same-company power users with DB access can
still do surgical fixes; the board-facing UX just refuses to be a
footgun. Covers J5 / S3 / S4.
2026-04-17 19:45:32 -04:00