Skip to content

feat: one-click GitHub, Jira and Linear credentials pane - #12

Merged
hkaltai merged 11 commits into
mainfrom
feat/one-click-local-integrations-sync
Nov 3, 2026
Merged

hkaltai merged 11 commits into
mainfrom
feat/one-click-local-integrations-sync

Conversation

@hkaltai

@hkaltai hkaltai commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds one-click GitHub, Jira and Linear integrations to the plugin as a hooks module (a Claude Code mod). It collects the same credentials the Altimate workspace web form asks for; there is no OAuth app. Stacked on #11; needs altimate integrations from altimate-code.

  • Band above the prompt: lists credentials found on the machine with Connect and Keep local. It says when a token stopped working, with Update.
  • Side pane (/integrations): one row per provider, with its state, account and whether it is local or synced.
    • Add credentials / Update expand the row into the workspace's form fields, pre-filled from the machine.
    • The secret line shows only ••••a1b2, with Create token ↗ and Paste from clipboard.
    • The footer has Connect & sync, Keep local and Cancel. The provider's error shows inline on rejection.
    • Rows also offer Sync, Allow sync and Disconnect.
  • Signed out: the band and pane offer Sign in to sync, and sync controls stay hidden until you sign in. Everything else works locally.
  • Keep local moves a workspace copy back to this machine and keeps it local until Allow sync. Update and Disconnect act on the connection this tool made, while the row shows the most recently validated one.
  • Status line: connected, synced and "needs attention" counts.
  • Secrets never pass through the mod: not its input, its state, toasts or logs. The CLI reads them.
  • The browser never opens without a click, and long work runs outside the button's time budget.

Test plan

  • claude plugin validate plugins/altimate-code passes, and tsc is clean.
  • claude plugin test plugins/altimate-code: 19 pass, 0 fail. Every test case is listed below.
  • Existing plugin suites pass: tests/*.sh (data-mode, refresh-skills, resolve-cli, session-start, warehouse-guard, workspace-tools), and pytest with 65 passed.
  • The CLI side (feat: altimate integrations command for GitHub, Jira and Linear altimate-code#1622) passes its 153 integration tests.
  • Ran in Claude Code (claude --plugin-dir) against a local Altimate backend with real GitHub and Jira credentials:
    • keep local
    • connect & sync
    • a rejected key
    • "Token rejected · Update", then the token updated on the same connection
    • Link workspace listing every workspace, one per line
    • a connection deleted in the web app, offered again because gh and jira-cli still hold the credential
All 19 test cases in hooks/integrations.test.tsx
  • rows offer Add credentials or Update, never a browser sign-in
  • the form shows a secret as a masked tail and keeps the form open on a refusal
  • a synced row cannot be blocked, and a hand-made one is only replaced by adding credentials
  • a row showing a newer hand-made connection still offers Update and Disconnect for the one this tool made
  • Keep local shows the notes the CLI returned
  • a failed action shows the reason the CLI printed on stdout, and a disconnect says what it removed
  • a sync with a failed provider shows its reason, and a timeout is not reported as a missing CLI
  • Sync all makes one call per provider with the per-provider timeout, and a failing provider does not stop the rest
  • the band connects what was found, after dropping what an earlier form left behind
  • the band does not connect when dropping earlier input fails
  • a credential found here is offered next to a hand-made connection
  • a session without a person at the prompt does not scan the machine
  • a status refresh that times out does not tell the user to install the CLI
  • connecting a found credential kept local shows the notes the CLI returned
  • a connect that is not linked to a workspace says so
  • a sync the CLI skipped shows its reason, not synced
  • an unlinked folder offers Link workspace; choosing one links, then syncs
  • a timed-out command is not rerun against the second binary
  • signed out: the band and pane offer Sign in to sync and no sync control; the click runs the browser sign-in

Known limitations and follow-ups

  • Linux and Windows: a kept-local secret goes to a 0600 file, not an OS keychain. Spec AC5 asks for the keychain only; this needs libsecret and DPAPI support.
  • Connection identity: a managed connection is matched by its name, Provider (from <hostname>), not a source.machine field. Renaming the host orphans it, and fixing that needs a backend field. Keychain keys are not yet namespaced per Altimate tenant.
  • Org policy "local only": not built. The backend has no policy source yet.
  • Workspace integrations list: the backend only replaces the whole list (PATCH /datamates/{id}). The CLI re-reads it just before writing, but two concurrent writers can still race until there is an incremental or conditional update.
  • Values starting with -: integrations set can't take one, because the argument parser reads it as a flag.
  • Token expiry: shown for GitHub only. Jira and Linear don't report it.
  • After a failed workspace removal: when Keep local can't remove the workspace copy, the credential is kept here and a warning says so, but status doesn't yet show both copies coherently.
  • Rollback: restoring the previous secrets after a failed keychain write is best effort. A failed restore is not reported.
  • Secret-name fallback: treating catalog fields with known secret names as secret applies only to freshly fetched catalogs, not to a cached one.

🤖 Generated with Claude Code

A hooks module adds an integrations band above the prompt, a side pane,
a status line entry and /integrations. It lists credentials found on the
machine, syncs them to the Altimate workspace or keeps them local, runs
the browser consent flow, and offers Reconnect when a token expires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hkaltai
hkaltai changed the base branch from main to tool-cli-plugin October 29, 2026 14:56
Each provider row expands into the workspace's own credential fields,
pre-filled from the machine, with Create token and Paste from clipboard
for the secret. Connect & sync, Keep local and Update replace the
browser sign-in buttons, and a rejected token shows in the row, the
band and the status line. Secrets never pass through the mod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hkaltai hkaltai changed the title feat: one-click GitHub, Jira and Linear integrations pane feat: one-click GitHub, Jira and Linear credentials pane Oct 30, 2026
hkaltai and others added 3 commits November 2, 2026 10:12
The pane said "synced to your Altimate workspace" whatever happened. It now
shows the CLI's message after connect/update (attached, not linked, or why
the attach failed) and reads each sync result, so a skipped or failed
provider shows its reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the project is not linked, the pane says so and offers Link
workspace: it lists the account's workspaces, links the chosen one, then
syncs so credentials already saved attach to it straight away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hkaltai and others added 2 commits November 2, 2026 21:30
- Sign in to sync when signed out; sync controls hidden until then.
- Keep local, Update and Disconnect offered per this tool's own connection.
- CLI messages, notes and warnings reach toasts on every action.
- Missing-binary fallback only when altimate is not installed; a timeout
  never reruns a command against altimate-code.
- Timeouts sized per command; Sync all gets its own budget.
- README, CHANGELOG and CONTRIBUTING describe the real flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lick-local-integrations-sync

# Conflicts:
#	plugins/altimate-code/CHANGELOG.md
- Sync all and Link workspace run one `sync <provider> --keep-synced` call
  per provider, each within its own timeout; one failure doesn't stop the rest.
- Band Connect and Keep local discard leftover drafts first, close an open
  form for that provider, and stop if the discard fails.
- A detected credential is offered beside a hand-made workspace connection.
- Status reads that finish late no longer overwrite a newer one; the scan at
  session start only runs in interactive sessions.
- README and CHANGELOG describe the Keep local outcomes and timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hkaltai
hkaltai marked this pull request as ready for review November 2, 2026 23:23
// A local credential that is not blocked only waits for a sync; a blocked one
// is unblocked and synced in the same click. One saved in the workspace has no
// local copy to keep: Update (above) offers Keep local, which moves it here.
if (!signedOut && one.state === 'connected' && one.sync === 'local') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: A sync === 'pending' row has no action left

The pane still renders this state as ready to sync (SYNC_LABEL.pending) and SyncState still declares it, but the only Sync button now requires one.sync === 'local', and the band no longer lists connected && sync === 'pending' either (line 624). A provider reported as pending therefore shows "ready to sync" with no control to sync it (only Sync all reaches it). Either keep 'pending' in this condition or drop the state and its label.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the CLI reports sync pending only together with state detected, never on a connected row, and the pane shows the sync label only for connected rows. A detected row offers Connect and Keep local instead, so no connected row can show a pending label.

async function runJson<T extends { message?: string | null }>($: EngineInterface, args: string[], timeoutMs?: number) {
const result = await runCli($, args, timeoutMs)
const data = parseJson<T>(result.stdout)
const failure = result.exitCode === 0 ? null : (data?.message ?? firstLine(result.stderr.replace(ANSI, '')) ?? 'failed')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: runJson ignores the JSON error field

failureText (line 122) treats a CLI body's error as a failure reason, but runJson reads only message. When connect/update/fields/set/discard fail with a body shaped { "error": ... }, the pane falls back to failed / not connected instead of the CLI's reason. Reuse failureText here (or also read data?.error) so both paths report the same message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: runJson now takes the failure reason from failureText, so a { error } body is reported the same way as { message }. Covered by a new test.

const result = await runAltimate($, ['workspace', 'list', '--json'])
const data = parseJson<{ ok?: boolean; workspaces?: WorkspaceChoice[] }>(result.stdout)
if (result.exitCode !== 0 || !Array.isArray(data?.workspaces)) {
await update($, message, () => failureText(result) === 'failed' ? 'Could not list workspaces.' : failureText(result))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Redundant double evaluation

failureText(result) is called twice in the same expression (the same pattern with failureText(link) appears at line 291). Each call re-parses stdout. Bind it once, e.g. const reason = failureText(result), and use reason in both branches.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: the reason is computed once and reused in listWorkspaces and linkWorkspace.

}

/** Forgets what was typed or pasted for a provider, so an abandoned form cannot leak into the next connect. */
async function discardEntries($: EngineInterface, provider: ProviderId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Duplicate discard logic, and failures are silent here

discardEntries and discardForAction (line 331) both run discard <provider> --json, and only the second reports the failure. Because cancelForm and openForm use this one and its catch {} swallows errors, a failed discard leaves the aborted form's stored secret in the CLI, which a later connect for that provider can pick up -- the band path deliberately refuses to connect in exactly this case. Fold both into one helper that returns the failure and surface it in the form.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: one discardEntries helper returns the failure and discardForAction builds on it. Opening a form shows the failure in the form, and cancelling reports it in the pane instead of ignoring it. Covered by a test.

async function loadForm($: EngineInterface, open: Expanded) {
try {
const { data, failure } = await runJson<Form & { message?: string }>($, ['fields', open.provider, ...(open.variant ? ['--variant', open.variant] : []), '--json'])
if (data && Array.isArray(data.fields)) await update($, form, () => data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Only fields is validated before the render dereferences the rest

loadForm accepts any payload that has a fields array, but the pane reads fields.variants.length (line 786) and fields.tokenUrl (line 760) unguarded. A payload without variants -- including a persisted form atom written by an older plugin version, since these atoms carry no shape tag -- throws during render. Normalize the missing pieces, e.g. { ...data, variants: data.variants ?? [] }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: loadForm turns a missing variants list into an empty one. A new session already starts with empty mod state, so no form from an earlier session is rendered.

description: 'One-click GitHub, Jira and Linear integrations for your Altimate workspace',
})
// A press that was running when an earlier session ended can never finish, so it must not block this one.
await update($, busy, () => null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Reset the rest of the per-session state too

Only busy is cleared here. expanded, form, typed and workspaces are persisted PluginState atoms and are never reset at session start, so a form left open when a session ended reappears in the next session with its typed values, and the workspace picker can show a stale list. Clearing them alongside busy matches the reason given for resetting busy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed, on purpose. Mod state is held per session, so a new session already starts without the old form or picker. session.start also runs when the plugin reloads or its worker restarts, and clearing the form there would close one the user is filling in and lose what they typed. The busy reset stays, because a press that was running can't finish after a restart. Explained in #16.

const who = ['github', 'jira', 'linear'].includes(one.id) ? nameOf(one.id as ProviderId) : one.id
if (one.result !== 'synced') return `${who}: ${one.reason ?? one.result ?? 'not synced'}`
const steps = one.steps ?? []
const bad = steps.filter(step => !step.ok).map(step => step.label)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Tolerate both step shapes in the sync report

steps is typed as objects here, but the sync report has also emitted each step as a plain label string (the previous revision normalized that). For a string step, step.ok and step.label are undefined, so every step counts as failed and the line reads ... undefined failed. A one-line normalizer (typeof step === 'string' ? { label: step, ok: !step.startsWith('Not linked') } : step) keeps both shapes working.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the CLI has emitted sync steps as { label, ok } objects since the first version of the command, so the string form is never produced.

- `gh`, jira-cli and Linear credentials on this machine, and GitHub, Atlassian or Linear MCP servers configured in Claude Code, are detected when a session starts.
- A band above the prompt offers **Connect**, **Keep local**, **Review** or **Later** for what it found, and shows **Token rejected · Update** when a saved token stops working. The status line shows how many integrations are connected.
- `/integrations` opens a side pane with one row per provider. **Add credentials** (or **Update**) opens a short form: type the non-secret fields, then **Create token ↗** opens the provider's token page and **Paste from clipboard** stores the token (shown masked afterwards). **Connect & sync** saves it, **Keep local** saves it without syncing, **Cancel** discards the form.
- A connected row offers **Sync**, **Allow sync** (after Keep local) and **Disconnect**. In a folder not linked to a workspace, **Link workspace** lists your workspaces and links the one you pick. When you are not signed in to Altimate, everything works locally: Connect behaves as Keep local, the sync buttons are hidden, and the band and pane offer **Sign in to sync**, which opens the browser sign-in on that click only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Documentation contradicts the signed-out behaviour

This says Connect "behaves as Keep local" when signed out, but the implementation hides Connect (integrations.tsx:668) and hides Connect & sync in the form (integrations.tsx:800); the CHANGELOG describes the hidden-button behaviour. Adjust the sentence so users are not looking for a Connect button that is never drawn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: the README now says Connect and Connect & sync are not shown when signed out (Keep local stays), matching the pane and the changelog.

@kilo-code-bot

kilo-code-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 9 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 8

Incremental review of 2a459e0: the only new change is Create token now passing --open to open-token-page under --json, which matches the stated CLI contract and keeps the browser opening strictly click-driven; the updated test asserts the flag, so no new finding arises from it. All nine earlier findings were re-checked against current HEAD and still reproduce. The credentials path remains sound: secrets only move through set-secret --from-clipboard, and nothing secret reaches argv, the state atoms, toasts or logs.

Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 856 sync === 'pending' still renders as "ready to sync" and is declared in SyncState, but Sync is only offered for sync === 'local' and the band no longer lists pending rows (line 624), so that state has no control to act on it.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 112 runJson reads only the JSON message, while failureText also handles error; a failed body shaped { "error": ... } becomes "failed"/"not connected".
plugins/altimate-code/hooks/integrations.tsx 270 failureText(result) is evaluated twice in one expression (same pattern at line 291), re-parsing stdout each time.
plugins/altimate-code/hooks/integrations.tsx 324 discardEntries duplicates discardForAction and swallows failures, so an aborted form's stored secret can survive Cancel/Open.
plugins/altimate-code/hooks/integrations.tsx 316 loadForm validates only fields; fields.variants (line 786) and fields.tokenUrl (line 760) are dereferenced unguarded, including for a persisted form atom with no shape tag.
plugins/altimate-code/hooks/integrations.tsx 601 Only busy is reset at session start; persisted expanded/form/typed/workspaces carry over between sessions.
plugins/altimate-code/hooks/integrations.tsx 557 syncText now assumes object steps; a plain-string step yields "undefined failed" (the normalizer was removed in 4542fad).
plugins/altimate-code/hooks/integrations.tsx 722 The inner Box flexDirection="column" restates the outer column Box on line 720; the buttons can render directly in the outer container.
plugins/altimate-code/README.md 96 Doc says Connect "behaves as Keep local" when signed out, but the code hides Connect and Connect & sync (lines 668, 800).
Files Reviewed (8 files)
  • CONTRIBUTING.md - no issues
  • plugins/altimate-code/.claude-plugin/plugin.json - no issues
  • plugins/altimate-code/CHANGELOG.md - no issues
  • plugins/altimate-code/README.md - 1 issue
  • plugins/altimate-code/hooks/hooks.json - no issues
  • plugins/altimate-code/hooks/integrations.test.tsx - no new issues
  • plugins/altimate-code/hooks/integrations.tsx - 8 issues (1 warning, 7 suggestions)
  • plugins/altimate-code/types/index.d.ts - no issues

Fix these issues in Kilo Cloud

Previous Review Summaries (2 snapshots, latest commit 3d6a04c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 3d6a04c)

Status: 9 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 8

Incremental review of 3d6a04c: the only new feature change is the Link workspace picker now laying out one workspace per line. That fix is correct (the outer Box is already a column); the one new note is a redundant inner wrapper. All eight earlier findings were re-checked against current HEAD and still reproduce. The credentials path remains sound: secrets only move through set-secret --from-clipboard, and nothing secret reaches argv, the state atoms, toasts or logs.

Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 856 sync === 'pending' still renders as "ready to sync" and is declared in SyncState, but Sync is only offered for sync === 'local' and the band no longer lists pending rows (line 624), so that state has no control to act on it.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 112 runJson reads only the JSON message, while failureText also handles error; a failed body shaped { "error": ... } becomes "failed"/"not connected".
plugins/altimate-code/hooks/integrations.tsx 270 failureText(result) is evaluated twice in one expression (same pattern at line 291), re-parsing stdout each time.
plugins/altimate-code/hooks/integrations.tsx 324 discardEntries duplicates discardForAction and swallows failures, so an aborted form's stored secret can survive Cancel/Open.
plugins/altimate-code/hooks/integrations.tsx 316 loadForm validates only fields; fields.variants (line 786) and fields.tokenUrl (line 760) are dereferenced unguarded, including for a persisted form atom with no shape tag.
plugins/altimate-code/hooks/integrations.tsx 601 Only busy is reset at session start; persisted expanded/form/typed/workspaces carry over between sessions.
plugins/altimate-code/hooks/integrations.tsx 557 syncText now assumes object steps; a plain-string step yields "undefined failed" (the normalizer was removed in 4542fad).
plugins/altimate-code/hooks/integrations.tsx 722 The new inner Box flexDirection="column" restates the outer column Box on line 720; the buttons can render directly in the outer container.
plugins/altimate-code/README.md 96 Doc says Connect "behaves as Keep local" when signed out, but the code hides Connect and Connect & sync (lines 668, 800).
Files Reviewed (8 files)
  • CONTRIBUTING.md - no issues
  • plugins/altimate-code/.claude-plugin/plugin.json - no issues
  • plugins/altimate-code/CHANGELOG.md - no issues
  • plugins/altimate-code/README.md - 1 issue
  • plugins/altimate-code/hooks/hooks.json - no issues
  • plugins/altimate-code/hooks/integrations.test.tsx - no blocking issues
  • plugins/altimate-code/hooks/integrations.tsx - 8 issues (1 warning, 7 suggestions)
  • plugins/altimate-code/types/index.d.ts - no issues

Fix these issues in Kilo Cloud

Previous review (commit 4542fad)

Status: 8 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 7

The credentials path is sound: secrets only move through set-secret --from-clipboard, and nothing secret reaches argv, the state atoms, toasts or logs. The main functional gap is a declared-but-unhandled sync state; the rest are redundancy, robustness and documentation issues.

Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 856 sync === 'pending' is still rendered as "ready to sync" and declared in SyncState, but no branch offers Sync for it and the band no longer lists pending rows.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks/integrations.tsx 112 runJson ignores the CLI's JSON error field that failureText handles, so some failures show "failed"/"not connected".
plugins/altimate-code/hooks/integrations.tsx 270 failureText(result) evaluated twice in one expression (also at line 291).
plugins/altimate-code/hooks/integrations.tsx 324 discardEntries duplicates discardForAction and swallows failures, so an aborted form's stored secret can survive Cancel/Open.
plugins/altimate-code/hooks/integrations.tsx 316 loadForm validates only fields; fields.variants (line 786) and fields.tokenUrl (line 760) are dereferenced unguarded.
plugins/altimate-code/hooks/integrations.tsx 601 Only busy is reset at session start; persisted expanded/form/typed/workspaces carry over between sessions.
plugins/altimate-code/hooks/integrations.tsx 557 syncText assumes object steps; a plain-string step yields "undefined failed" (regression from the previous normalizer).
plugins/altimate-code/README.md 96 Doc says Connect "behaves as Keep local" when signed out, but the code hides Connect and Connect & sync.
Files Reviewed (8 files)
  • CONTRIBUTING.md - no issues
  • plugins/altimate-code/.claude-plugin/plugin.json - no issues
  • plugins/altimate-code/CHANGELOG.md - no issues
  • plugins/altimate-code/README.md - 1 issue
  • plugins/altimate-code/hooks/hooks.json - no issues
  • plugins/altimate-code/hooks/integrations.test.tsx - no blocking issues
  • plugins/altimate-code/hooks/integrations.tsx - 7 issues
  • plugins/altimate-code/types/index.d.ts - no issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 42.7K · Output: 6.6K · Cached: 422.5K

The Link workspace picker laid the workspaces out in a single row, so the
ones past the pane's width were cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sahrizvi sahrizvi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. One-click GitHub, Jira and Linear credentials via the hooks module look correct against the integrations CLI contract in AltimateAI/altimate-code#1622 (head c6d1aa3).

Checked and holding: no secret value reaches the mod's state, props, toasts, status line or argv. Secret fields come back as a masked tail only, are never rendered as an input, and are set only by set-secret --from-clipboard in the CLI. The browser opens only on a click, and only to the CLI's hardcoded provider token pages. Commands run as argv with no shell. Long work runs off the press handlers, and session start doesn't wait for the scan (skipped for -p/SDK). Field names, statuses and exit codes match the CLI.

Four minor findings are inline. None blocks:

  • The busy guard is check-then-set across awaits: safe to do in a follow-up (the CLI lock keeps data consistent; only redundant work).
  • An older CLI without integrations shows a help/logo line rather than "update altimate": edge input.
  • Allow sync, Update → Keep local, the row's Connect and Rescan are never pressed in the tests: test-only.
  • The no-rerun-after-timeout guarantee depends on the wording of the error text, which the tests fake: test-only.

Nits are in a separate PR comment.

/** Connect (or keep local) every provider the band found, one after the other. */
function connectFound($: EngineInterface, providers: Provider[], localOnly: boolean) {
later($, async () => {
if (await read($, busy)) return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (logic): the busy guard is check-then-set across awaits

connectFound reads busy, awaits update(message), and only sets busy inside the loop. Two presses that land before the band redraws (Connect then Keep local, or a double click) both pass the check and run two connect sequences. The same check-then-set pattern is in syncAll (196-197), act (212-213) and withProgress (373-374), though with fewer awaits in between. The CLI's lock and the upsert of the managed connection keep the result consistent, so the cost is redundant provider checks and uploads, and the last press wins rather than the first.

Suggestion: set busy immediately after the check, before any other await, in every guarded entry point.

Not a blocker: the data stays consistent; only redundant work results.

await update($, status, () => next)
showStatusLine($, next)
} else {
const failure = parseJson<{ message?: string }>(result.stdout)?.message ?? firstLine(result.stderr.replace(ANSI, ''))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (CLI contract): an older CLI without integrations gives no actionable message

A released CLI that lacks the subcommand fails fast: yargs .strict() prints help (with the logo) to stderr and exits 1, so nothing hangs. Here, though, the pane then shows firstLine(stderr), which is the first line of that help/logo banner. The user is never told to update altimate. The altimate-code fallback doesn't apply either, because altimate exists.

Suggestion: when the exit code is non-zero and stdout is not JSON (optionally when stderr contains Unknown argument), show something like "altimate is too old for one-click integrations; update it".

Not a blocker: edge input. The README states the requirement, and session start is not slowed.

expect(await ui.find({ key: 'jira-add' })).toBeDefined()
expect(await ui.find({ key: 'jira-update' })).toBeUndefined()
expect(await ui.find({ key: 'jira-off' })).toBeUndefined()
expect(await ui.find({ key: 'linear-allow' })).toBeDefined()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (testing): several row actions are never pressed

This test checks that linear-allow exists, but no test presses Allow sync (unblock then sync, stopping after a failed unblock). No test opens an Update form and presses Keep local, which is the path that sends connect --local-only instead of update (integrations.tsx:466) and moves a workspace credential back to this machine. No test presses a row's single Connect (without --local-only) or Rescan either.

Suggestion: add press tests that assert the argv sequence for each.

Not a blocker: test-only.


// A command that cannot start rejects; only that moves on to the next name, so a
// timeout never reruns a command that may have taken effect.
const MISSING_BINARY = /ENOENT|not found|no such file|(cannot|could not|failed to) (start|spawn|run)/i

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (testing / robustness): the no-rerun guarantee rests on error wording

Besides ENOENT, this regex matches any rejection containing not found, could not run or failed to run. The two timeout tests (integrations.test.tsx:383, 496) fake the rejection with a hand-written 'timed out while the command was still running'. Nothing pins the engine's real timeout or abort wording against this pattern, and a match would rerun a possibly non-idempotent connect against altimate-code.

Suggestion: narrow the fallback to a spawn-not-found error (ENOENT) only, or test against the engine's documented rejection.

Not a blocker: test-only. The documented timeout path behaves correctly today.

@sahrizvi

sahrizvi commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Review nits and notes (not blocking; the approval and inline findings are in the review above)

Nits

  • integrations.tsx:800 — Signed out, an Update form still shows Update, which runs update and gets the CLI's "Sign in to Altimate first" refusal. Hide it as Connect & sync is hidden.
  • README.md:96 — Says Connect "behaves as Keep local" when signed out. The code hides Connect and Connect & sync instead (integrations.tsx:668, 800). The CHANGELOG is right.
  • integrations.tsx:152 — The status line counts MCP-only available providers as "found", while the band calls them "available via Claude MCP".
  • integrations.tsx:823 — Rescan (a read-only detect) toasts "Integrations updated" (the summarize default).
  • integrations.tsx:174-191 — Sync all and Link workspace call sync for all three providers, including not_found ones, so the toast carries "nothing detected or connected" lines.
  • integrations.tsx:101-104 — parseStatus checks only providers; workspace.* is dereferenced unguarded at render (631, 702, 708).
  • integrations.tsx:137-146 — A failed refresh keeps the last good status and shows the failure in the message line, without marking the rows as possibly stale. Actions re-run the CLI against live state, so nothing wrong lands, but a "last checked" hint or dimming would be clearer.
  • integrations.tsx:518-520 — When several found providers need input, only the first one's form opens.

Note (not a finding)

  • Every interactive session runs integrations detect --json in the background (a machine scan, a state write under the CLI lock, a workspace read). It doesn't block the session, but several sessions opened together queue on the CLI's 30 s lock.

{choices && !running && (
<Box flexDirection="column">
<Text dimColor>Link this folder to:</Text>
<Box flexDirection="column">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The inner column Box is now redundant

The outer Box on line 720 is already flexDirection="column", so this wrapper only restates the same direction and adds a nesting level. Rendering the workspace buttons directly in the outer Box (dropping this element and its closing tag) gives the same one-per-line layout with one container fewer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #16: the workspace buttons are rendered directly in the outer column, dropping the redundant inner Box; still one per line.

open-token-page only opens the browser with --open under --json, and the
button press is the explicit request to open it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from tool-cli-plugin to main November 3, 2026 11:39
@hkaltai
hkaltai merged commit 6ca6ecf into main Nov 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants