6a37f41dfddeae3fb8d190a65c987fee12f77441
496
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6a37f41dfd |
fix(threads): unify wait-state ownership across thread types
`hasUnresolvedWaitStatesForAgent` only joined through tasks, so approval-only threads and ad-hoc threads where the agent is the requesting party or an explicit participant were invisible. Result: the heartbeat scheduler silently resumed agents that the board had asked to wait on, Co-Founder approval chains deadlocked on the agent side, and any new gate that relies on the query inherits the same hole. Root-fix the query at the SQL layer. `hasUnresolvedWaitStateForAgent` now returns true when any of the three ownership predicates hold: - task thread whose task.assignee_agent_id = agent - approval thread whose approval.requested_by_agent_id = agent - any thread on which the agent is a recorded participant Each branch is an EXISTS subquery so the query short-circuits on first match. The thread-integration helper delegates here so the heartbeat scheduler and the `beforeToolCall` gate share one source of truth rather than reimplementing half-policies in parallel. |
||
|
|
e1693899a2 |
feat(runtime): wire pi-agent-core hooks end-to-end
The worker previously constructed `new Agent({systemPrompt, model, tools,
messages, streamFn})` and stopped there — transformContext,
beforeToolCall, afterToolCall, sessionId, thinkingLevel, and steer/
followUp were all dark. Approval enforcement, artifact projection,
auto-compaction, and mid-run steering all had to reach the agent
through other, non-canonical channels (event observers, prompt-block
injection at prepareRun, API-layer gates). That left the whole pi
contract at one-tenth utilisation and forced business logic into the
wrong seams.
This change rebuilds the contract assembly layer:
- `pi-agent-factory` builds the Agent with the full option surface:
sessionId (taskSessionId), thinkingLevel (mapped from
agent.metadataJson), and three RPC-proxied hooks.
- Each hook shuttles a thin payload over the existing worker RPC
channel into `RuntimeBridge`, which routes to `hook-handlers`:
* beforeToolCall → approval-gate policy (blocks destructive
tools while a pending approval is outstanding).
* afterToolCall → filesystem artifact projection, previously
nested in the tool_execution_end event observer.
* transformContext → pass-through today; compaction wires in
next commit.
- ParentMessage gains `steer-run` / `followup-run`; `WorkerRunner`
keeps a runId→Agent registry so main can inject mid-run messages
into the active pi loop.
`worker-types` extends the rpc-request method enum; `runtime-bridge`
loses the per-event artifact projector and gains a `findActiveRunForAgent`
lookup used by the upcoming inbox → steer path. No public main API
changes yet — the wiring is the foundation for the next five commits.
|
||
|
|
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.
|
||
|
|
e0e43d6127 |
refactor(session): nuke dead sessionParams / sessionDisplayId pipeline (v43)
These columns and the messages that plumbed them existed for the CLI-
adapter era — Gemini's resume id and provider-specific session params
rode the run-finished envelope back onto the session row so the next
turn could resume. Pi Mono dispatches every provider through pi-ai and
emits neither value. The worker has been posting literal null on every
finish, finalizeRun has been writing null into task_sessions, and
rotateSession has been carrying null forward.
Remove the wire, the writer, the carry-forward, and the columns:
- v43 migration rebuilds task_sessions without session_params_json /
session_display_id, and runs without session_display_id. Follows
the idempotent "create new, copy, swap, reindex" pattern used by
v41. context_snapshot_json is preserved (also unreferenced today
but outside this cleanup's scope).
- RunJob / EnqueueRunPayload / ChildMessage / worker-types zod
schema no longer carry the fields. finishRun() / createNewSession()
signatures tighten in lockstep.
- rotateSession starts the new session with fresh counters and no
carry-forward.
- turn-context resolveSession returns just the session id.
- TaskSessionRecord / RunRecord lose the dead fields.
498/498 unit tests pass. typecheck + build clean.
|
||
|
|
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. |
||
|
|
dac59f56c2 | update | ||
|
|
1261c2431f |
refactor(heartbeat): close the silent-skip loop; durable skip reason
Symptom: scheduler logs `[heartbeat] Waking agent X (interval: 120s)`
every 15 seconds for three ICs, but no `[heartbeat] Starting taskless
heartbeat run` ever follows. Agents are wedged, UI shows nothing, and
the operator has no way to see why.
The loop had three compounding flaws:
1. `startAgentHeartbeatRun` returned `string | null`. Every gate
rejection was a silent `return null` — most were `logger.debug`,
which prod log levels filter out. Neither the scheduler nor the
renderer could tell the difference between "ran fine" and
"blocked by budget / no tasks / pending approval." The function
name promised "run or not" and the enum of reasons lived in
comments, not in the type.
2. The scheduler logged `[heartbeat] Waking agent ${name}` BEFORE
calling start, so even pure no-ops spammed the log. The "wake"
wasn't actually a wake; it was an attempt.
3. `markHeartbeatAt` is only called when a run starts. When start
returns null, `lastHeartbeatAt` stays frozen. On the next 15s
tick, `now - lastHeartbeatAt >= intervalMs` is still true, and
the scheduler fires again. The loop was a fixed-point at "skip →
log → skip → log" running 8 times per minute until something
external changed.
Root fix (data structure + visibility):
- `startAgentHeartbeatRun` now returns a discriminated union:
`{ status: "started"; runId } | { status: "skipped"; reason }`,
with `HeartbeatSkipReason` closed over 16 named reasons. Every
exit path carries its reason. The type tells operators what the
function can actually do; the enum tells them what it did.
- Scheduler holds a per-agent `attemptedAtByAgent` map. The interval
check uses `max(lastHeartbeatAt, lastAttemptedAt)`, so a streak of
skips still respects the heartbeat interval. Failed attempts cool
down the same as successful runs.
- On `skipped`, the scheduler persists `{reason, at}` into
`agents.runtime_state_json` under `heartbeatSkip`. On `started`,
it clears the field. The UI can read per-agent skip state from
the snapshot without a new IPC or table; operators finally see
"agent waiting: no_actionable_tasks" instead of a blank screen.
- The `Waking agent` log is gone. Its replacement prints once per
actual skip, with the reason inline (`[heartbeat] X not ready:
no_actionable_tasks`), and the existing `Starting taskless
heartbeat run for X` keeps marking the happy path.
`wakeAgentIfPossible` keeps its external `string | null` contract;
only the internal call site adapts. No caller outside of
`heartbeat.ts` / `scheduler-heartbeat.ts` / `scheduler.ts` needed
changes.
File sizes: heartbeat.ts 290, scheduler-heartbeat.ts 133. All under
the 400 cap. Tests: 603/603 green. Typecheck clean.
|
||
|
|
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.
|
||
|
|
2ace9183b0 |
refactor(session): drop dead task-scoped session columns (v41)
Sessions have been agent-scoped since v34, but the schema kept four
dead task-scoped columns that every session code path had to pretend
were meaningful:
- tasks.active_session_id (mirrored agents.session_id)
- tasks.session_json (superseded by task_sessions.session_params_json)
- tasks.session_display_id (superseded by task_sessions.session_display_id)
- task_sessions.task_id (always empty-string on new rows)
v41 drops all four via SQLite table rebuild — the only safe path when
multiple columns need to go in one step and indexes / FK directions
need to be preserved. `PRAGMA foreign_keys = OFF` spans the swap
window so cascade-delete does not fire mid-rebuild. The migration is
idempotent and also backfills the optional task columns
(task_type / requires_user_review / retry_count / last_failure_at /
started_at / completed_at / cancelled_at / checkout_run_id /
execution_locked_at) before rebuilding so it works whether or not
`ensureOptionalColumns` has run yet on this install.
Along with the schema change, every code path that read or wrote
those columns has been rewired to the agent-scoped equivalents:
- task-sessions-crud.ts: deleted updateTaskSession,
getTaskSession, getActiveTaskSession, updateTasksActiveSessionForAgent;
added updateSessionParams(sessionId, ...) for the worker's
finalizer. ensureAgentSession and createNewSession no longer touch
task_id.
- database-base.ts: facade drops the now-unused pass-throughs.
- snapshot.ts / task-queries.ts / tasks-status.ts / entity-crud.ts:
SELECT projections no longer pull session_json / session_display_id
/ active_session_id from tasks.
- tasks-crud.ts (TaskRow) and shared/types/records-core.ts
(TaskRecord): sessionJson / sessionDisplayId fields removed.
- TaskSessionRecord: taskId field removed.
- turn-context.ts::resolveSession: no more
updateTasksActiveSessionForAgent mirror; sessionParams read directly
from the current session via getSessionById.
- run-finished-helpers.ts::finalizeRun: stamps sessionParams via
updateSessionParams on the agent's current session, resolved through
agents.session_id.
- session-management-service.ts: rotateSession stops calling
updateTasksActiveSessionForAgent.
Test suite updated to use the new session-id API. Existing tests used
`getActiveTaskSession(taskId)` and `createNewSession({ taskId, ...})`
as probes; they now use `getSessionById(sessionId)` and the taskId-less
createNewSession input.
Tests: 603/603 green. Typecheck clean. Migration tested on fresh DB
(runs v1→v41 cleanly) and idempotent re-apply (detects absent dead
columns, skips).
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
ff5e22092a |
refactor(session): agent-scope rotation + accumulation; auto-register file artifacts
Sessions are already agent-scoped post-v34, but the code kept routing
rotation / accumulation / handoff through the task indirection. Heartbeat
runs (no task) silently skipped all three, letting taskless transcripts
grow past the model's context window — the 7M-token explosion.
- DB adds getSessionById(sessionId) — direct lookup bypassing
tasks.active_session_id
- session-management-service: generateHandoff(session) is now agent-scoped,
walking recent runs across tasks + heartbeats; getHandoffForAgent(agentId)
is the canonical lookup, getHandoffContext(taskId) is a thin shim for
callers that still hold a task id
- turn-context.ts::resolveSession drops the `if (!task)` early return:
rotation evaluation runs on the agent session unconditionally
- run-finished-helpers.ts::accumulateAndRotateSession drops the `!task`
gate: tokens accumulate and rotation fires for every run, keyed on the
agent session, not the task
- Rotation evaluator contract: evaluatePreRunSessionRotation(agentId,
connectorId) → { rotated, handoff, sessionId }; old (taskId, connectorId)
signature removed
IPC contract tightening: ipc-support::emitEvent becomes a best-effort
sink. "Render frame was disposed" during renderer reload is a lifecycle
signal, not a logic error; silently drop it instead of letting it abort
downstream event emissions.
Artifact projection: pi's write/edit tool results now upsert a
`work_products` row via a new agent-runtime/artifact-projector.ts. No
more agents writing 54 files into the workspace that the UI never sees.
Migration v40 relaxes work_products.task_id to nullable so heartbeat
artifacts can register too.
File size: split agent-event-projection.ts out of runtime-bridge.ts to
keep the bridge under the 400-line cap after adding the projector hook.
Tests: 604 unit + integration green; generateHandoff callsites updated
for the single-argument signature; artifact-projector has pure-function
coverage via the projector module's own shape.
|
||
|
|
61fb4b947f | Update README.md | ||
|
|
3f5f20f8c1 |
fix(test): correct stale section name in notification-service batched test
Test 7 asserted navigation.section === "orgchart", but commit
|
||
|
|
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.
|
||
|
|
1f7230fe87 |
chore: drop non-code junk (pi-mono-reference, runtime state, root duplicates)
Remove files that should never have been tracked: - pi-mono-reference/: 688-file copy of an upstream monorepo, zero imports, used only as offline human reference - build/electron-builder.runtime.json: environment-specific build artifact with hardcoded local paths - state/cancel-signal-state.json: runtime state written by the app - prompt.txt, logo.svg (root): exact duplicates of the canonical copies under .planning/archive/ and assets/ Add state/ to .gitignore so future runtime state stays out. |
||
|
|
34ca5b77f1 |
docs(readme): cut marketing fluff; one fact per sentence
Removing what a reader doesn't need:
- Subtitle shrunk from three sentences to one. "Run an AI company from
your desktop." The two follow-up sentences ("Give it a mission.
Watch agents hire, delegate, and deliver.") were restating the same
thing twice.
- "Built with quiet authority. Ship your AI company." closing tagline
deleted. AI-flavored vapor.
- Feature openers trimmed. Each feature is now [bold title] + [one
sentence]. The previous paragraphs qualified, reassured, and
explained their own engineering trade-offs ("context travels with
the work instead of being re-rendered from scratch every run"). A
reader skimming the README doesn't need the trade-off; the architecture
section has it.
- "No plaintext, no ~/.pi/agent/auth.json, no scattered env files"
rhetorical triad replaced with "Credentials are SecretVault-encrypted
in SQLite." One fact, stated once.
- "Agents are at work before your coffee" deleted. Cute-cringe.
- "the company respects the chain of command" deleted. Hollow.
- "What is AgentCompany?" section compressed from three paragraphs to
two sentences.
- Image alt text shortened where it was describing the screenshot to
the screenshot ("AgentCompany Dashboard — active agents, tasks, and
company metrics" → "AgentCompany dashboard"). Alt text is for screen
readers landing on the page, not for search engine padding.
- Top-nav link list trimmed from 4 entries to 2 (Download, Website).
The in-page anchors were noise.
Facts preserved verbatim: 26 providers with correct per-group counts
(5/18/3), the two execution modes with their approval gate names,
architecture tree, tech stack, first-run steps, build commands.
Nothing factual removed.
Line count: 186 → ~170, but the bigger win is prose density — every
paragraph now says exactly one thing.
|
||
|
|
ff7d1e68a1 |
docs(readme): rewrite for pi-mono runtime + 26-provider matrix + execution modes
The previous README described an app that no longer exists: - Listed "Claude Code / Codex CLI / Gemini CLI" as the supported runtimes — all three were removed from the app months ago; pi-mono replaced them as the sole agent runtime (commit |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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.
|
||
|
|
ab731e64d2 |
fix(onboarding): actually probe shell PATH for node; stop returning Electron's bundled version
The `checkNodeInstalled` IPC handler was hardcoded to
`{ installed: true, version: process.version }`. That meant:
1. The NodeCheckStep in onboarding ALWAYS passed, regardless of what
was actually on the user's machine.
2. The "Node.js vX.Y installed" success banner showed the **Electron
runtime's** bundled Node version, not the one the agent's bash
tool will invoke. A user with no shell Node saw "Node.js v22.x
installed" and moved on, then every agent bash call that executed
`node -e 'fetch(...)'` (the pattern the API prompt-builder strongly
recommends in `api-docs/header.en.md`) crashed at runtime.
Replaced the stub with an `execFile("node", ["--version"])` probe that
runs in the same PATH the bash tool will later inherit. Added a 3s
timeout so a hung PATH entry doesn't stall onboarding. On failure the
handler returns `{ installed: false, version: null }` — the existing
NodeCheckStep already renders the platform-specific install
instructions (brew / winget / nvm) in that case.
No UI change; the fake-pass was the bug. Covers the onboarding audit
item about misleading health checks.
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
02ba887610 |
fix(runtime): stop gating pi tool calls; Co-Founder approvals live at API layer
The Co-Founder beforeToolCall hook (introduced in
|
||
|
|
4cfb926971 |
refactor(schema): split 852-LOC migration ledger into era files
schema.ts had grown to 852 LOC — migration history is append-only, so
the file gets bigger every release. Organize by era without fragmenting
the history.
| File | LOC | Contents |
|-----------------------------------|-----|---------------------------------------|
| schema.ts | 195 | helpers, runMigrations, |
| | | ensureOptionalColumns, seed, init |
| migrations/types.ts | 47 | Migration/Deps types, column/table |
| | | helpers |
| migrations/v1-v15.ts | 50 | createMigrationsV1V15 — v1..v15 |
| migrations/v16-v25.ts | 157 | createMigrationsV16V25 — v16..v25 |
| migrations/v26-v34.ts | 158 | createMigrationsV26V34 — v26..v34 |
| migrations/v35-v37.ts | 326 | createMigrationsV35V37 — three heavy |
| | | table-rebuilds (runs/tasks/skills) |
Each era exports a factory `createMigrationsVxxVyy(deps)` that takes
`{ db, readAll }` — migrations' `fn` bodies captured those names via
closure in the original; factory pattern preserves the closure shape
byte-for-byte while making the dependency flow explicit.
Master `schema.ts` imports all four and concatenates them into the
final ascending list. Order preserved including the quirky v31 -> v33
-> v32 -> v34 sequence that predated my changes.
All 37 migrations' `version`, `name`, and `sql`/`fn` bodies are
byte-for-byte identical. No behavior change; 578 tests green.
`database-base.ts` (449 LOC) kept as-is. It's a thin linear pass-
through facade — every method is 1–3 lines. Splitting a facade of
this kind by forcing mixins or abstract classes costs more than it
saves. Not a god component in the Linus sense (one responsibility:
delegate); flagged here so future readers know it's deliberate.
|
||
|
|
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.
|
||
|
|
dbdec86cdd |
refactor(database): split 5 god modules into domain-scoped files
Five files under `src/main/database/` exceeded 400 LOC by combining multiple responsibilities. Split along natural fault lines. Zero behavior change — each original `build*Methods` factory still returns the same method surface, now via spread of smaller builders. | Original | Split into | LOC each | |-----------------------------------|-------------------------------------|----------------| | tasks-crud.ts (640) | tasks-crud / task-sessions-crud / | 250 / 252 / | | | task-queries | 177 | | agents-hierarchy.ts (487) | agents-hierarchy / agent-permissions| 350 / 151 | | company-settings.ts (476) | company-settings / company-metrics | 122 / 370 | | documents.ts (426) | documents / config-revisions | 318 / 126 | | runs.ts (413) | runs / run-logs / run-events / | 245 / 96 / | | | cost-entries | 82 / 52 | AppDatabase call sites unchanged: - db.tasks.saveTask, db.tasks.ensureAgentSession (task-sessions-crud), db.tasks.listAgentTasks (task-queries) - db.agents.getChainOfCommand, db.agents.hasAgentPermission - db.company.getCompanySettings, db.company.getStandupReport (company-metrics) - db.documentsMod.saveDocument, db.documentsMod.recordConfigRevision - db.runs.createRun, db.runs.appendRunLog (run-logs), db.runs.appendRunEvent (run-events), db.runs.recordCostEntry (cost-entries) `database-base.ts` (449) kept as-is — thin facade where every pass-through is one line; splitting adds mechanical files without behavior benefit. Will revisit if it grows further. Aggregator files (`tasks.ts`, `agents.ts`, `company.ts`) each import and spread the new sub-builders. No renamed exports. |
||
|
|
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.
|
||
|
|
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). |