Commit Graph
154 Commits
Author SHA1 Message Date
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
ZacharyZhang-NY 50a7196b3e refactor(org): drop legacy adapterLabels / claude_compat fallback; unify connector label resolution
OrgChart and AgentRoster both carried a `claude_compat`-prefix fallback
chained against `adapterLabels[connectorId] ?? connectorId` to paper
over the pre-pi-mono connector id space (claude_local / codex_local /
gemini_local / claude_compat_2..10). The migration to pi-mono's catalog
retired those ids months ago — the fallback is dead code that still
runs `String.prototype.startsWith` on every render and forks the label
resolution into two subtly different shapes in two files.

Replaced with `formatConnectorLabel(connector, fallbackId)` in
lib/formatters.ts, used by both OrgChart and AgentRoster.

Also in this commit (typography polish bundled because touching the
same render paths):

- OrgChart connector label: 9px uppercase tracking-0.08em → 11px
  tracking-tight. The 9px chip was unreadable at default zoom.
- OrgChart zoom controls: `+` / `−` / "Fit" mixed text + character ops
  replaced with Plus / Minus / CornersOut icons (imported from the
  existing icon barrel) + a focus-visible ring for keyboard users.

No rendering changes for the non-cosmetic path (status dots, card
layout, edges). The removed `adapterLabels` map was exported from
orgTreeLayout.ts and had no other importers.

Covers J1 / J3 / J4 / J6 from the team-page audit.
2026-04-17 19:43:12 -04:00
ZacharyZhang-NY 41132e2da4 fix(team): don't auto-open CEO edit modal after onboarding
useCompanyController.handleOnboardingDone() pre-selected the newly-created
lead agent via setSelectedAgentId(). OrganizationSections.tsx uses the same
`selectedAgentId` state as the Edit modal open flag (`open={!!selectedAgent}`),
so the first visit to the Team section after onboarding auto-opened
`Edit: CEO` with no user action.

Removed the pre-select. Roster highlight on navigation still works via
the explicit `setSelectedAgentId(entityId)` path in the navigation slice;
the Edit modal now only opens when the user clicks a roster row, matching
user expectation.

Scope note: this does not yet split `selectedAgentId` into separate
`focusedAgentId` + `editingAgentId` states; clicking a roster row still
opens the editor immediately. That UX split belongs to the Jony/Jobs
Team-page audit pass, not this single-line fix.
2026-04-17 19:11:28 -04:00
ZacharyZhang-NY 00b0e0aaed refactor(ui+skills): split import.ts + primitives.tsx god files
Two more > 400 LOC modules split along responsibility seams. Zero
behavior change — public APIs re-export from the original file paths,
so every consumer import stays working.

| Original                              | Split into                    | LOC each |
|---------------------------------------|-------------------------------|----------|
| src/main/skills/import.ts (458)       | import (orchestrator) /       | 132      |
|                                       | discovery (findSkillMdFiles,  | 153      |
|                                       | buildLocalInventory)          |          |
|                                       | materialize                   | 24       |
|                                       | import-local                  | 147      |
|                                       | import-url                    | 85       |
| src/renderer/components/ui/           | primitives (barrel re-export) | 11       |
| primitives.tsx (419)                  | Card / Eyebrow / Badge /      | 9–59     |
|                                       | Spinner / Panel / Field /     | each     |
|                                       | Input / Select / TextArea /   |          |
|                                       | ActionButton / SectionButton  |          |

`createSkillImporter` still returns `{ importFromSource, importFromGitHub,
importFromLocal, importFromUrl, cleanupSkillFiles, parseGitHubUrl,
findSkillMdFiles, buildLocalInventory }` — every existing consumer
resolves unchanged.

`primitives.tsx` is now a pure barrel re-exporting the 11 primitives.
Consumers can migrate to direct imports (`../ui/Card`) opportunistically
but the barrel path keeps working.
2026-04-17 16:35:30 -04:00
ZacharyZhang-NY f012f8e880 refactor(skills): drop company_skills.markdown column, read from disk via IPC
`company_skills.markdown` stored the full SKILL.md body. The on-disk
file under `profile/skills/{companyId}/{slug}/SKILL.md` is what pi-
coding-agent actually reads via `loadSkills` at runtime. The DB column
was a duplicate — import wrote to both, renderer read from DB. Any
discrepancy between disk and DB was invisible until the agent behaved
differently from what the user saw in SkillDetail.

Disk becomes the single source of truth. DB keeps name / description /
metadata / source info / file inventory — the cheap lookup row. UI reads
SKILL.md content live via a new IPC channel.

## Migration v37 (rebuild company_skills without markdown)

Table-rebuild pattern matching v35/v36: `fn:` form, PRAGMA walk for
drift columns, row-count assertion before DROP, `defer_foreign_keys`
for the `agent_skill_assignments(skill_id)` reference. Indexes
recreated. Idempotent via `schema_migrations` ledger.

Existing DB rows' markdown content is discarded — but the on-disk
SKILL.md files are the authoritative copy, so no user-visible data
loss. Any agent using those skills keeps working; the UI just starts
reading from disk instead of the DB cache.

## Code changes

- `src/main/database/skills.ts`: drop `markdown` from read mapping,
  upsert input type, INSERT column/bind list, ON CONFLICT DO UPDATE.
- `src/shared/types/records-core.ts`: `SkillRecord.markdown` removed.
- `src/main/skills/import.ts` + `import-github.ts`: drop `markdown:`
  from 4 `saveCompanySkill` call sites. Disk-write via
  `materializeSkillFiles` unchanged (it's what the file agent reads).
- `src/main/skills/index.ts`: expose `skillFilesDir` for the new IPC
  handler to resolve `${profile}/skills/${companyId}/${slug}/SKILL.md`.
- `src/shared/contracts/channels.ts` + `schemas-execution.ts` +
  `desktop-api/skills.ts`: new `getSkillMarkdown` channel
  `{ companyId, slug } → DesktopResult<string>`.
- `src/preload/index.ts`: expose `getSkillMarkdown(companyId, slug)`.
- `src/main/ipc-skill-handlers.ts`: handler validates schema, ensures
  company, resolves path, `readFile`, returns content. Fails closed
  with `SKILL_NOT_FOUND` on ENOENT, `SKILL_READ_FAILED` on other
  read errors.
- `src/renderer/components/skills/SkillDetail.tsx`: `useEffect` fetches
  markdown via IPC when the skill detail loads; shows "Loading…" until
  resolved, surfaces read errors. Preserves `stripFrontmatter`.
- Tests: drop `markdown:` from 2 fixture sites
  (`skill-types.test.ts`, `skill-aware-assignment.spec.ts`).

## Verification

- typecheck / lint / 578 tests green
- `grep 'skill\.markdown' src/` → 0 DB-record reads
- `grep '\.markdown\b' src/main/database/skills.ts` → 0
- Remaining `.markdown` references are all `ghSkill.markdown` (GitHub
  fetcher's intermediate struct feeding `materializeSkillFiles` to
  disk) and parameter names in `stripFrontmatter(markdown: string)` /
  fs-writer helpers — none read from a DB row.
2026-04-17 16:22:31 -04:00
ZacharyZhang-NY 7e1d4199ee refactor(schema): make runs.task_id nullable, delete heartbeat workaround
The NOT NULL constraint forced heartbeat runs to synthesize a fake task
row (`[Heartbeat] X — Check for work`) so the FK could resolve. Three
layers of code — acquireHeartbeatTask, findReusableHeartbeatTask,
cleanupDuplicateHeartbeatTasks, plus the is_heartbeat flag column + UI
filter — existed to paper over one bad constraint. Remove the
constraint and the whole workaround vanishes.

SQLite table rebuild with data preservation. Live production DB, no
backup — migration is atomic, idempotent, and verified before DROP.

## Migration v35 (rebuild runs with nullable task_id)

Uses `fn:` form so it can:
- Snapshot `COUNT(*) FROM runs` before copy
- Walk `PRAGMA table_info(runs)` to detect drift columns (v34 declared
  a column set; any additive ALTER since v14 shows up here)
- Assert `COUNT(*) FROM runs_new == beforeCount` before `DROP TABLE runs`
- `PRAGMA defer_foreign_keys = ON` so run_events(run_id) FK defers to
  commit time, by which runs has the same id set under the new name
- Recreate all 6 indexes on the new table

If the assertion fails, the migration throws inside the existing
`transaction()` wrapper. SQLite rolls back the whole migration, old
`runs` table is untouched, `schema_migrations` never records v35. Next
app start retries cleanly.

## Migration v36 (drop tasks.is_heartbeat + synthetic rows)

Runs now carry `task_id = NULL` for heartbeat dispatches. The
`is_heartbeat` flag is dead data going forward.

1. `UPDATE runs SET task_id = NULL WHERE task_id IN (SELECT id FROM tasks WHERE is_heartbeat = 1)`
2. `DELETE FROM tasks WHERE is_heartbeat = 1` (synthetic rows, not real work)
3. Rebuild `tasks` without the `is_heartbeat` column (same defer-FK +
   row-count-assert pattern as v35)

## Code cleanup

- `RunRecord.taskId: string | null`; `createRun` / `cancelRunForDeletion`
  / `finishRun` guard every `UPDATE tasks` path with a null-check.
- `TaskRecord.isHeartbeat` removed; every SELECT / filter / UI reference
  dropped.
- Delete `acquireHeartbeatTask` / `findReusableHeartbeatTask` /
  `cleanupDuplicateHeartbeatTasks` — dead after the constraint lifts.
- Delete `src/main/orchestration/heartbeat-task.ts` (pure workaround
  constant file).
- `heartbeat.ts` creates runs with `taskId: null` when the agent has no
  assigned work. Turn context already supports nullable task
  (heartbeatFallbackSection); prompt surface unchanged.
- `validateTaskCreation` drops the `isHeartbeat` parameter — user-
  authored + automation-authored tasks are never heartbeat.
- Renderer: `run.taskId ? taskMap.get(...) : undefined` in Dashboard /
  RunCenter / RunDetail / RunQueue.
- Delete `tests/unit/heartbeat-coalescing.test.ts` (12 tests that
  exercised the deleted DB methods).
- Drop 1 isHeartbeat-bypass test from `task-quality-gates.test.ts`.

## Grep verification

- `isHeartbeatTaskTitle src/` → 0
- `acquireHeartbeatTask src/` → migration comment only
- `is_heartbeat src/` → schema.ts migration history only
- `isHeartbeat src/` → 0

Tests: 578/578 (was 591 — delta is 13 tests that tested deleted code).
2026-04-17 16:16:08 -04:00
ZacharyZhang-NY 5547c29491 update 2026-04-17 14:03:56 -04:00
ZacharyZhang-NY 7fc707ab8c fix(onboarding): stabilize connector-model state so native <select> stays open
The CEO-runtime model dropdown (and the team-model pool) collapsed the
instant a user clicked to open it whenever the app received a background
snapshot refresh. Root cause:

  useEffect(executionReadyConnectors, ...)
    → loadModels() loads catalog for every ready connector
    → setConnectorModelsById(Object.fromEntries(entries))
      — always a brand-new object reference, even if the catalog
        returned from the IPC is byte-identical to the previous load
    → connectorModels useMemo picks up a new reference
    → modelOptions useMemo picks up a new reference
    → <Select> re-renders its options children
    → Chromium's native <select> treats DOM mutation of <option>
      children as "popup stale" and closes the dropdown.

Because snapshot refreshes fire on every domain-changed event
(credentials-changed, run-status, heartbeat-status, etc.), the user
effectively could not hold the dropdown open long enough to pick a
model.

Fix: structurally compare the freshly-loaded catalog against the
previous state and bail out of setState when the shape is identical
(same connector ids, same per-connector model lists in order, same
id/label/enabled flags). Same guard on the error-path reset so an
already-empty map does not trigger a re-render either.

typecheck 0 / lint 0-0 / test:unit 508/508 / build 0-0.
2026-04-17 10:28:19 -04:00
ZacharyZhang-NY 9b45b12508 fix(onboarding): drop "Default" sentinel from model picker; auto-select real model
The CEO-runtime model dropdown on the Company onboarding step rendered
a {value:"", label:"默认 / Default"} sentinel as the first option, a
holdover from the adapter era when "" meant "let the connector pick".
Under pi-mono every connector carries a defaultModel and every run
must bind a concrete model id, so the sentinel is now misleading —
picking it left selectedModel empty and the wizard would advance
without a valid model.

- CompanyStep: drop the sentinel from the options array; the dropdown
  now shows only real models (or a single "Loading..." entry while
  the per-connector fetch is in flight).
- useConnectorModelSelection: when the connector's current model is
  not enabled, auto-select the connector's defaultModel if enabled,
  otherwise the first enabled model. An empty enabled list still
  falls back to "" so the CompanyStep hint UI behaves correctly.

Result: Fire Pass now shows "Kimi K2.5 Turbo (Fire Pass)" as the
sole option (pre-selected). Every other connector picks its catalog
default up-front.

typecheck 0 / lint 0-0 / build 0-0 / test:unit 508/508.
2026-04-17 09:54:46 -04:00
ZacharyZhang-NY e94939a562 test(connector-id): migrate fixtures and assertions to pi-mono ids
Replaces legacy adapter ids (codex_local / claude_local / gemini_local /
claude_compatible) across ~42 test files with pi-mono equivalents:
- codex_local → openai-codex
- claude_local → claude-pro
- gemini_local → gemini-cli
- claude_compatible → anthropic

Hand-rewrites where fixture semantics changed:
- connector-matching: TASK_TYPE_PREFERENCE ordering rebased (claude-pro
  first for code/content/management; google first for research)
- connector-model-policy: getModels(provider) from pi-ai instead of the
  deleted adapter.module.models array
- session-management + edge-case-session: adapter exemption removed;
  rotation now uniform under pi-mono
- ipc-core-admin-handlers: CSV compat model replaced with pi-ai anthropic
- connectors.spec: lookup by openai-codex id (catalog now starts with claude-pro)
- onboarding-model-pool: listProviderModels(provider) helper; label text
  changed to "ChatGPT Plus/Pro (Codex)"
- database integration: added vault insecure-fallback env + electron mock
- command-paths: uses new @main/connector-helpers normalizeCommandValue
- pi-mono-migrations: added `as string` casts for 3 negative-existence
  assertions verifying legacy ids were removed

Drops:
- pnpm-workspace.yaml packages: [] (was "packages/*"; directory empty)
- obsolete CONNECTOR_STRENGTHS import; .module.models probing

Verification:
- pnpm typecheck → 0 errors
- pnpm lint → 0 errors / 0 warnings
- pnpm build:verify → 0 errors / 0 warnings
- pnpm test:unit → 508/508 pass
- pnpm test:integration → 96/96 pass
- All 6 adapter hard gates pass (NOT FOUND / 0 matches / empty)
- pi-ai / pi-agent-core / pi-coding-agent all resolved @0.67.6
2026-04-17 02:36:53 -04:00
ZacharyZhang-NY 4a120f4fc6 feat(settings): drive providers panel from pi-mono catalog
ProvidersPanel reads all 26 CONNECTOR_CATALOG entries and groups them by
auth kind (OAuth / API key / Azure / AWS / Vertex). ProviderRow
dispatches to the matching dedicated form component per auth kind. AWS,
Azure, Vertex forms rewritten with Tailwind classes and a shared field
style module so the Settings pane renders with the same aesthetic as the
rest of the app.
2026-04-17 02:19:56 -04:00
ZacharyZhang-NY 7fdd3c16bf feat(renderer): rewrite providers UI on pi-mono catalog shape
Onboarding ProvidersStep now reads the 26-entry CONNECTOR_CATALOG and
renders featured connectors (claude-pro, openai-codex, anthropic, openai,
amazon-bedrock) up front with a collapsible advanced list for the
remaining 21. Each auth kind gets a dedicated card component: OAuth
button flow, inline API-key form, and a cloud deep-link to Settings.
Delete legacy Claude/Codex/Gemini hardcoded cards and the compatible-API
slot rotation. Swap ModelManagement Settings panel for the new
ProvidersPanel driven by credentialsList IPC.
2026-04-17 02:16:35 -04:00
ZacharyZhang-NY d42d895f2a chore(lint): fix react-hooks/exhaustive-deps warnings
- App.tsx: replace per-field ui.navItems/ui.paletteQuery with the ui
  object so the memo captures every accessed field.
- ConsoleView.tsx: add eslint-disable for the logRevision dep; it is
  intentionally tracked to force ref re-evaluation.
2026-04-17 01:44:50 -04:00
ZacharyZhang-NY 70c2a3aa9e fix(renderer): type SidebarView t prop as TranslationFn
`t` is invoked with a two-arg `(key, fallback)` signature elsewhere;
use the shared `TranslationFn` type so those call sites compile.
2026-04-17 01:30:16 -04:00
ZacharyZhang-NY 1f310a121e i18n(automation): localize RuleEditor and WorkflowEditor form strings
Adds ~40 automation.ruleEditor.* and automation.workflowEditor.* keys
to en.ts + zh.ts (labels, placeholders, error messages, action labels).
RuleFormModal and WorkflowFormModal now route every hardcoded English
string through useT(), including validation errors and modal titles.

Closes remaining Z-B i18n work after the prompt-builder locale
dispatch (cf92e0c), dept-keywords typing (a78bdb9), and sidebar/
formatters/notification/skills i18n landed earlier.
2026-04-17 01:13:19 -04:00
ZacharyZhang-NY 75022f82c5 refactor(renderer): route all Phosphor icon imports through ui/icons barrel
Create src/renderer/components/ui/icons.ts as the single re-export point
for every Phosphor icon the renderer touches, then rewrite the 95 files
that previously imported directly from "@phosphor-icons/react" to pull
from the relative-path-to-barrel instead. The TitleBar commit earlier
in this series already used ui/icons; this commit extends the pattern
to the rest of the renderer.

Benefits now wired in:
  - one location where bundle tree-shaking can be reasoned about,
  - typos like Briefcase vs BriefCase show up against a finite list,
  - re-theming or swapping icon sets is a one-file edit,
  - a lint rule that flags raw @phosphor-icons/react imports can be
    added later without mass-rewriting call sites.

The barrel also re-exports the `Icon` type so call sites that type
icon props against the Phosphor component type work unchanged.

Test count stays at 363 passing; typecheck stays at 156 errors.
2026-04-16 23:37:35 -04:00
ZacharyZhang-NY d513497341 i18n(skills): localise SkillsSection strings
SkillsSection had nine hardcoded English strings (header, filter
placeholder/aria-label, empty-state copy, button labels). Move them to
skills.* keys in en.ts/zh.ts and swap the component to useT().
2026-04-16 23:35:21 -04:00
ZacharyZhang-NY dd007760b8 refactor(ui): promote BudgetBar and InlineConfirmDelete to shared ui/
BudgetBar and ConfirmDelete were only sitting in organization/helpers.tsx
but have no organization-specific behaviour. Move them under
components/ui/ and keep the old exports as re-exports so existing
importers continue to resolve. HeartbeatSection stays in helpers.tsx
because it owns agent-specific heartbeat IPC.
2026-04-16 23:34:12 -04:00
ZacharyZhang-NY 61aba41e77 refactor(renderer): rename EntityWorkspace to TwoPaneLayout for clarity 2026-04-16 23:33:55 -04:00
ZacharyZhang-NY 933fcf27d0 refactor(runs): extract useRunLogs hook from RunCenter
RunCenter had three useEffects juggling log-chunk loading, tailing, and
older-chunk fetches. Move the whole lifecycle into useRunLogs, which
uses a single effect + AbortController to scope every IPC call to the
current run and pauses polling while document.hidden.
2026-04-16 23:32:02 -04:00
ZacharyZhang-NY 0c9d8269c1 refactor(dashboard): consolidate BudgetAlertBanner dual source into helper 2026-04-16 23:31:58 -04:00
ZacharyZhang-NY c6935ff983 refactor(ui): move AttentionRow to ui and reuse in Inbox 2026-04-16 23:29:58 -04:00
ZacharyZhang-NY 736cff9371 refactor(titlebar): replace hand-drawn restored-window SVG with Phosphor Copy icon
P3-56 (renderer): the maximized-state button used a custom 3-rect SVG to
render the 'restored window' glyph while the minimize and close buttons
used Phosphor icons. Swap the SVG for Phosphor's Copy icon so all three
titlebar buttons share the same icon family and weight, and the visual
bundle no longer carries a one-off raster-like SVG block.
2026-04-16 23:29:04 -04:00
ZacharyZhang-NY 3edb2fc6c1 refactor(main): extract getOrCreateStore in RuntimeBridge
P3-56: three call sites repeated the same 'lookup → fall back to
new → set if missing' dance for session stores. The duplication hid the
fact that the fallback allocation in the second and third sites could
be wasted when the entry already existed. Extract a single
getOrCreateStore helper that does one map lookup, one conditional
construction, and always returns the cached instance.
2026-04-16 23:27:57 -04:00
ZacharyZhang-NY 326bfb3e38 refactor(communication): extract ComposerShell from MessageComposer and TaskDiscussionComposer 2026-04-16 23:27:41 -04:00
ZacharyZhang-NY aa1204babe refactor(organization): extract shared GoalForm and ProjectForm shells
Introduce goal-form/ and project-form/ directories each holding a
single shared shell that handles both create and edit modes via a
discriminated mode prop. The existing GoalCreator/GoalEditor and
ProjectCreator/ProjectEditor become thin wrappers that forward props
to the shared shell, eliminating duplicated state, JSX, and save/
delete handlers across the four files.
2026-04-16 23:27:40 -04:00
ZacharyZhang-NY 6809fa13af polish: reformat AutomationActivityLog JSX; InlineNotice alert role for actionable warns (P1-20, P3-63) 2026-04-16 23:12:57 -04:00
ZacharyZhang-NY a7ad4e5a8b polish: composite breadcrumb keys; pause run-log polling when tab hidden (P3-58, P3-59) 2026-04-16 23:11:25 -04:00
ZacharyZhang-NY 96ab72472c ConfirmDialog: accept confirmTone/confirmLabel/cancelLabel props (P3-57) 2026-04-16 23:10:48 -04:00
ZacharyZhang-NY 95c9deb778 refactor(useCompanyController): drop eslint-disable comments via getState() in callbacks (P2-24)
setters now read inline via useAppStore.getState() so exhaustive-deps
rule is satisfied without disables; 7 disable comments removed.
2026-04-16 23:10:36 -04:00
ZacharyZhang-NY ca9b94b5a6 titlebar: await windowMaximize promise instead of setTimeout race (P2-39) 2026-04-16 23:10:19 -04:00
ZacharyZhang-NY d8c2e10005 i18n(sidebar): localise company delete confirmation + Yes/No buttons (P3-48)
Delete-company confirmation text now uses sidebar.deleteCompanyConfirm key
with {name} placeholder; Yes/No buttons route through common.yes/no.
2026-04-16 23:09:15 -04:00
ZacharyZhang-NY 3a1f79324d toast: cap message max-height with scroll; assertive role for danger toasts (P2-38, P3-55) 2026-04-16 23:09:11 -04:00
ZacharyZhang-NY cd85419d1d oauth/inbox: use native ordered list; explicit severity fallback (P2-45, P2-48) 2026-04-16 23:08:44 -04:00
ZacharyZhang-NY e54aee8d8b style(App): localise console toggle labels, inline ease via tailwind class (P3-46, P3-47)
aria-label/title now use app.hideConsole/app.showConsole i18n keys; the
grid transition timing function moves from inline style into an
ease-[cubic-bezier(...)] utility.
2026-04-16 23:08:32 -04:00
ZacharyZhang-NY 7290338b11 composer: unify getAgentInitials; replace invalid focusRingColor with focus:ring-ring (P2-49, P2-32) 2026-04-16 23:07:40 -04:00
ZacharyZhang-NY 5da9945792 skills: replace hand-rolled primary button with ActionButton; use Input primitive (P2-33, P2-34) 2026-04-16 23:06:35 -04:00
ZacharyZhang-NY 7eba08cfb9 refactor(ExecutionSections): extract buildTaskEscalation policy from onClick (P2-38)
Move the blocked-vs-strategy approval-type decision out of JSX into a pure
buildTaskEscalation(task) helper so the escalation policy is testable and
not embedded in the click handler.
2026-04-16 23:05:58 -04:00
ZacharyZhang-NY 9e6aa61e09 feat(i18n): dev-mode warn on missing translation keys, default-locale fallback (P3-54)
Missing keys now log a console.warn in dev; at runtime the default-locale
dict is consulted before falling back to the raw key so zh users are not
shown literal keys when a zh translation is absent.
2026-04-16 23:05:20 -04:00
ZacharyZhang-NY dc368962f4 feat(desktop): preserve error.code via DesktopError class in unwrap (P3-52)
Callers can now catch DesktopError and inspect err.code to distinguish
auth/validation/unknown failures; plain Error fallback kept for message.
2026-04-16 23:05:00 -04:00
ZacharyZhang-NY 41fee50c55 EntityWorkspace: normalize typography to match Panel (20px/bold), use Eyebrow primitive (P1-10, P2-27) 2026-04-16 23:04:52 -04:00
ZacharyZhang-NY 84d11582a3 chore(store): use import.meta.env.DEV instead of process.env.NODE_ENV (P3-53)
Vite-native idiom; added vite/client triple-slash reference to env.d.ts
so TS knows about ImportMeta.env.
2026-04-16 23:04:36 -04:00
ZacharyZhang-NY b2eed3e75a fix(budget-policy-input): convert dollars to cents via string parsing (P2-35)
Avoid floating-point drift from parseFloat * 100 by parsing digits then
concatenating; dollarsToCents returns null for invalid input.
2026-04-16 23:02:37 -04:00
ZacharyZhang-NY 296c72eb3f composer/settings: use ActionButton iconOnly send; replace deprecated navigator.platform with lib/platform (P1-9, P2-46) 2026-04-16 23:02:29 -04:00
ZacharyZhang-NY b0b0cfa35b refactor(keyboard): extract shared nextIndexFor, drop duplicate handleKeyDown body (P2-28)
createRovingTabIndex and useRovingTabIndex now share one key-to-index
resolver; keeps both exports for test + React usage without drift risk.
2026-04-16 23:02:17 -04:00
ZacharyZhang-NY a063f5ed91 refactor(useCompanyData): extract filterByCompany helper, drop 14 inline filters (P2-34)
One filterByCompany<T>() helper replaces 14 nearly-identical snapshot.X
.filter((item) => !currentCompany || item.companyId === currentCompany.id)
expressions.
2026-04-16 23:01:34 -04:00
ZacharyZhang-NY f1dd9786e5 onboarding: remove AsciiAnimation decoration and 'by ELYDORA' signatures (P1-17, P1-18); move hero-drift keyframes to global CSS (P3-60) 2026-04-16 23:01:11 -04:00