diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 73562db86..d7d57c729 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -102,6 +102,14 @@ const openExternal: Platform["openExternal"] = (value) => { } const restart: Platform["restart"] = async () => { + // Amicode webview (framed): the error page's Restart must also restart the + // underlying opencode server (issue #382) — browser reload alone leaves the + // panel in the error state when the server is down/unreachable. + if (window.parent !== window) { + window.parent.postMessage({ source: "amicode", kind: "command", command: "amicode.restartServer" }, "*") + // Give the extension host a moment to restart the server before reloading + await new Promise((resolve) => setTimeout(resolve, 800)) + } window.location.reload() } diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 2fc609541..530dbde32 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -296,7 +296,16 @@ export const ErrorPage: Component = (props) => { hideLabel />
- diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 8cc8ec9db..b9e18399f 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -2069,42 +2069,54 @@ export default function Page() { {(_id) => ( - - !location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled() + 0 || rolled().length === 0} + fallback={ +
+
All messages are rolled back
+
+ Use the rolled-back bar below to restore a message and continue. +
+
} - centered={centered()} - setContentRef={(el) => { - content = el - autoScroll.contentRef(el) - - const root = scroller - if (root) scheduleScrollState(root) - }} - userMessages={visibleUserMessages()} - setHistoryAnchor={(handlers) => { - captureHistoryAnchor = handlers.capture - restoreHistoryAnchor = handlers.restore - }} - anchor={anchor} - setRevealMessage={(fn) => { - revealMessage = fn - }} - setScrollToEnd={(fn) => { - scrollToEnd = fn - }} - /> + > + + !location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled() + } + centered={centered()} + setContentRef={(el) => { + content = el + autoScroll.contentRef(el) + + const root = scroller + if (root) scheduleScrollState(root) + }} + userMessages={visibleUserMessages()} + setHistoryAnchor={(handlers) => { + captureHistoryAnchor = handlers.capture + restoreHistoryAnchor = handlers.restore + }} + anchor={anchor} + setRevealMessage={(fn) => { + revealMessage = fn + }} + setScrollToEnd={(fn) => { + scrollToEnd = fn + }} + /> +
)}
diff --git a/packages/app/src/pages/session/composer/session-revert-dock.tsx b/packages/app/src/pages/session/composer/session-revert-dock.tsx index d8f022a53..0ae52fb30 100644 --- a/packages/app/src/pages/session/composer/session-revert-dock.tsx +++ b/packages/app/src/pages/session/composer/session-revert-dock.tsx @@ -21,10 +21,19 @@ export function SessionRevertDock(props: { collapsed: true, }) + // Don't force-collapse on every items change — that makes the dock + // un-dismissable (issue #381). Only collapse when the revert first appears + // (0 → >0) or the revert boundary changes to a different message. + let prevLen = 0 + let prevFirstId: string | undefined createEffect(() => { - props.items.length - props.items[0]?.id - setStore("collapsed", true) + const len = props.items.length + const firstId = props.items[0]?.id + if (len > 0 && (prevLen === 0 || firstId !== prevFirstId)) { + setStore("collapsed", true) + } + prevLen = len + prevFirstId = firstId }) const toggle = () => setStore("collapsed", (value) => !value) @@ -58,7 +67,7 @@ export function SessionRevertDock(props: { {preview()} -
+
+ { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + setStore("collapsed", true) + }} + aria-label="Dismiss" + title="Collapse" + />
@@ -132,7 +156,7 @@ export function SessionRevertDock(props: { {preview()}
-
+
} size="large" @@ -150,6 +174,21 @@ export function SessionRevertDock(props: { store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse") } /> + } + size="large" + variant="ghost-muted" + onMouseDown={(event) => { + event.preventDefault() + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + setStore("collapsed", true) + }} + aria-label="Dismiss" + title="Collapse" + />
diff --git a/packages/opencode/src/server/amicode/connections.ts b/packages/opencode/src/server/amicode/connections.ts index 23ea7f9d1..467ff69c5 100644 --- a/packages/opencode/src/server/amicode/connections.ts +++ b/packages/opencode/src/server/amicode/connections.ts @@ -152,7 +152,7 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [ name: "Google", icon: { kind: "svg", svg: CONNECTION_ICONS["google"] }, validator: "google", - authShape: "browser", + authShape: "token-only", }, { id: "google-drive", @@ -160,7 +160,7 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [ name: "Google Drive", icon: { kind: "svg", svg: CONNECTION_ICONS["google-drive"] }, validator: "google-drive", - authShape: "browser", + authShape: "token-only", }, ] @@ -436,7 +436,7 @@ function renderStatus( if (icon) out.icon = icon const name = nameForId(id) if (name) out.name = name - if (id === "google" || id === "google-drive") out.auth_methods = ["browser"] + if (id === "google" || id === "google-drive") out.auth_methods = ["token", "browser"] return out } let state: ConnectionState @@ -471,7 +471,7 @@ function renderStatus( if (icon) out.icon = icon const name = nameForId(id) if (name) out.name = name - if (id === "google" || id === "google-drive") out.auth_methods = ["browser"] + if (id === "google" || id === "google-drive") out.auth_methods = ["token", "browser"] return out } diff --git a/packages/ui/src/amicode/connection-picker.tsx b/packages/ui/src/amicode/connection-picker.tsx index 15c961d22..94ece6270 100644 --- a/packages/ui/src/amicode/connection-picker.tsx +++ b/packages/ui/src/amicode/connection-picker.tsx @@ -19,7 +19,18 @@ export function ConnectionPicker(props: { const [token, setToken] = createSignal("") const pickedEntry = () => props.catalog.find((e) => e.id === picked()) - const isBrowserEntry = () => pickedEntry()?.authShape === "browser" + // Google now supports both token and browser — treat it as token in the picker + // so users can paste a token like Claude/Slack/GitHub, with browser as alternative + const isBrowserEntry = () => { + const entry = pickedEntry() + if (!entry) return false + if (entry.id === "google" || entry.id === "google-drive") return false + return entry.authShape === "browser" + } + const isGoogleEntry = () => { + const entry = pickedEntry() + return entry?.id === "google" || entry?.id === "google-drive" + } const submitCustom = async (e: Event) => { e.preventDefault() @@ -139,25 +150,52 @@ export function ConnectionPicker(props: { - {picked()} - setToken(e.currentTarget.value)} - class="amc-input amc-input--compact" - /> -
- - + +
+ + }> + {/* Google: token (paste like Claude) + browser alternative */} +
+ {picked()} + setToken(e.currentTarget.value)} + class="amc-input amc-input--compact" + /> +
+ + +
+
— or —
+ -
- + + }>
{picked()} diff --git a/packages/ui/src/amicode/connections.ts b/packages/ui/src/amicode/connections.ts index f7df41da5..5c0626c6a 100644 --- a/packages/ui/src/amicode/connections.ts +++ b/packages/ui/src/amicode/connections.ts @@ -443,7 +443,7 @@ export type ConnectionFormKind = "base-url-token" | "pasqal-credentials" | "toke export function connectionFormKind(id: string): ConnectionFormKind { if (id === PASQAL_ID) return "pasqal-credentials" - if (id === GOOGLE_ID || id === GOOGLE_DRIVE_ID) return "browser" + if (id === GOOGLE_ID || id === GOOGLE_DRIVE_ID) return "token-only" if (id === SLACK_ID || id === GITHUB_ID || id === LINEAR_ID) return "token-only" if (isCustomConnectionId(id)) return "custom" return "base-url-token" @@ -472,7 +472,7 @@ export function customConnectionPayload(name: string, token: string, url?: strin * legacy single method implied by the card's form kind. */ export function connectionAuthMethods(view: ConnectionView): ConnectionAuthMethod[] { if (view.authMethods && view.authMethods.length > 0) return view.authMethods - if (view.id === GOOGLE_ID || view.id === GOOGLE_DRIVE_ID) return ["browser"] + if (view.id === GOOGLE_ID || view.id === GOOGLE_DRIVE_ID) return ["token", "browser"] return connectionFormKind(view.id) === "pasqal-credentials" ? ["credentials"] : ["token"] } @@ -484,6 +484,9 @@ export function methodEntryKind(id: string, method: ConnectionAuthMethod): Metho if (method === "browser" || method === "device-code") return "none" if (method === "credentials") return connectionFormKind(id) if (method === "token") { + // Google now supports token like Slack/GitHub — return token-only even though + // connectionFormKind previously returned browser for backwards compat + if (id === GOOGLE_ID || id === GOOGLE_DRIVE_ID) return "token-only" const kind = connectionFormKind(id) if (kind === "token-only" || kind === "custom" || kind === "browser") return kind return id === PASQAL_ID ? "pasqal-token" : "base-url-token"