diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 8e65c7f34..d354d2635 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -28,7 +28,7 @@ "packages/app/src/design-polish.css": "6b860316d2d57a4a9a89c1ff2772de45eca0d0cef2bd3e2ea8572d817e1d1910", "packages/app/src/desktop-menu.ts": "350e0e9f2e4294bfe1b1f357b3cd2ea0908b14d20ca1e79fda2305db1958fb24", "packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c", - "packages/app/src/index.css": "2c11df3dcebb381358e06626094b5dda3c35688ad7940d643b2cbb6cff1ea9e9", + "packages/app/src/index.css": "727656c5e7ff7d7c7ff4fc95a3ec7c9148a34f1f974a58a297285e870519be5d", "packages/app/src/theme-preload.test.ts": "d9e4e96dd39a3491493637682611bedf6ddf4b6e4dbdddcb60bf006211b137a1", "packages/app/test-browser/empty-workspace-landing.test.ts": "83d19ecf5da37c234dbde1a004432865204d7f412a335c4e470a07e73d9bcda4", "packages/app/test-browser/keybind-migration.test.ts": "192418880d432ff62d15fd80f6250ece6233e43d0dbe11d5a4fbb5f4d9e45a06", @@ -98,6 +98,7 @@ "packages/app/src/components/amicode-default-model.tsx": "16a2de1feb12e07416fe5fbb25879391f912ee0909b553c868c1b531f2ed1f2d", "packages/app/src/components/amicode-defaults-capsule.tsx": "94dc081bc47abb7e50566fcd8e9e53124f9d7632d61f93eff4f1627e0c2efb2d", "packages/app/src/components/amicode-file-ref-bridge.tsx": "11c17d7fdb29d16530e188b13ee8af728f37d9adfbb518084106ce50cc0c527a", + "packages/app/src/components/concise-mode-toggle.tsx": "3eff0c5557143500a27d828a08d1efe09a8c99aac9b6e2f9950bfe70c76162a1", "packages/app/src/components/connection-banner.tsx": "c793966e1f9d6f307af697032b235c2ff65afaa04f6cc232e40ce45e5745ded4", "packages/app/src/components/dialog-command-palette-v2.css": "85984957adaf15efcc0828310f3a0b438cedaaba776a9fec2eddb3ad83487fa2", "packages/app/src/components/dialog-command-palette-v2.tsx": "e12da3e5e9e503089dd9139b20e04f6b566cf74fa445f23a2e193f5883f7c5f3", @@ -115,7 +116,7 @@ "packages/app/src/components/pane-bridge.tsx": "756b3306b51c963fac6778716f6c6c4626f0f3bf75c93b7cb4cab58ea0bf3d4e", "packages/app/src/components/profile-popover.tsx": "eff742e90544455f13234a9351bb1b82189663cba2cd3172bdcea1a8a37284e8", "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "946b718c2164da394602f26e4d9919d752da0d0b9bee714f345ba16c274542da", - "packages/app/src/components/prompt-input-v2.tsx": "5ab46e41bda4fa11e99f06c37df76c1fd3cce061c6ba7a9f85bee369a965b98a", + "packages/app/src/components/prompt-input-v2.tsx": "4a51a7823755514497497def61fd4b890f29e08573d7d727b303d31318f3b265", "packages/app/src/components/prompt-input.tsx": "97a9bc55b46cf1f2d7c3e38110e736f7fa9db7f89121d3c013b9650a60d382d4", "packages/app/src/components/prompt-project-selector.tsx": "0157c954b938bce2b5aca9a82f4a72338e361f4c9027e55a7fd5a14a1654d294", "packages/app/src/components/report-bug-button.css": "909809b6241d53c386aa4c8ea34539ba162a8998fff755834c783025bc53a5fd", @@ -242,7 +243,7 @@ "packages/app/src/utils/retry-import.ts": "591bfd3e28f8afa9c4d9f0ca118d20e875cb1dce574c0ae25615994c4fe57a99", "packages/app/src/utils/serialize-session.test.ts": "f8fe318f195dca251eae3f4899f4a5881c3523522f59e02fef208588391f4048", "packages/app/src/utils/serialize-session.ts": "c303c678500cdb64c7aa6928a0bd984c441a36705fe8152368a0e575cbcb459f", - "packages/app/src/utils/server-compat.ts": "01413e13b87485770eb3dbec0b882aec4c8247dafc4dc74b7b4669fa5b20cf1a", + "packages/app/src/utils/server-compat.ts": "1cccb1aabde1bce6ee382c33db0dea9273a9519747b805b771dec060d3071b39", "packages/app/src/utils/session-message.ts": "96f8f3dca62f70215ac2dccbc8d27cd5b247af3ecea1ac5d34a74ffc8af5d4d3", "packages/app/src/utils/start-prompt.ts": "809d8ad0ab6de76a97503f8368ade25f771431be3f92eb8f039a2c65244ab20f", "packages/app/src/utils/vscode-explorer-icon-theme.test.ts": "351deb0ff77e62d2585858105aaf34ab621298511798fcb626abee97bd644fe6", @@ -622,7 +623,7 @@ "packages/app/src/components/prompt-input/placeholder.ts": "6b2af23a0588e7970cd0dd059fc60c3671c907a76ba3db660483a932f1dce79d", "packages/app/src/components/prompt-input/slash-popover.tsx": "a1800173edebcd419802796470f7270acaf0ef689a72f1b81a1cedfd8968337d", "packages/app/src/components/prompt-input/submit-stream-gap.test.ts": "68a94b03e75625531b2f2ad161ef9d0b93365f9edae154a797f82031b5bfc062", - "packages/app/src/components/prompt-input/submit.ts": "6f14368b0d481bdf10a2711e08da41010edd9f67ca8af90b930c096a56778b89", + "packages/app/src/components/prompt-input/submit.ts": "ea6818c3d0570f382b355b31c23ce3c654d726b277c3413dcc2d343c2f4ea555", "packages/app/src/components/session/context-warning-banner.tsx": "f5fab4d7a9536238b70d64488fc30d44f6a4948fdcffd910a9666f4b6c296019", "packages/app/src/components/session/index.ts": "21473290d4a1a3d0670fd878372ddf21ff22d4a0b30d1aef6c81ea906c1488b4", "packages/app/src/components/session/panel-menu.tsx": "42f323046b7375ae158023a51506038eb15f2e1545c407f45acd47446a8ef3e0", @@ -858,7 +859,9 @@ "packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts": "10bcb79b117beebee04184fb48c1ad789e782e2f7ae4b0c68603edc00f03aee9", "packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts": "7da08d58765cdf2b6ce4648fc70de371402949f5bb7b75696f674d43f2d5365b", "packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts": "8baf16cad0992c5351b61b0c0fb27903bb25b17e1b25fc7c5d77f807c24413a6", - "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "b37ff4136c4aea85b23083bf441f03e09820e3e4cdd95e4118ca570d8435b94f" + "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "b37ff4136c4aea85b23083bf441f03e09820e3e4cdd95e4118ca570d8435b94f", + "packages/app/src/context/concise-mode.ts": "b03ac887222543e590d31781c944e69aca97bff32a661751dd817d46bf4ead64", + "packages/app/src/context/concise-mode.test.ts": "534258a1d82d66bd8b482bb3a8ca6a24d508328505abd0f7f52e1b75cdf57fa2" }, "symlinks": [ "packages/app/public/amico.svg" @@ -873,10 +876,10 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-09-24T04:46:38.771Z", + "extracted_at": "2026-09-30T02:26:16.851Z", "per_package": { "packages/app": { - "A": 186, + "A": 189, "M": 157, "D": 0 }, @@ -922,7 +925,7 @@ } }, "counts": { - "overlay_total": 851, + "overlay_total": 854, "deletions": 7, "server_coupled": 0 }, @@ -1017,6 +1020,7 @@ "packages/app/src/components/amicode-default-model.tsx": "A", "packages/app/src/components/amicode-defaults-capsule.tsx": "A", "packages/app/src/components/amicode-file-ref-bridge.tsx": "A", + "packages/app/src/components/concise-mode-toggle.tsx": "A", "packages/app/src/components/connection-banner.tsx": "A", "packages/app/src/components/dialog-command-palette-v2.css": "M", "packages/app/src/components/dialog-command-palette-v2.tsx": "M", @@ -1777,7 +1781,9 @@ "packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts": "M", "packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts": "M", "packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts": "M", - "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "M" + "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "M", + "packages/app/src/context/concise-mode.ts": "A", + "packages/app/src/context/concise-mode.test.ts": "A" }, "upstream_base_archive_sha256": "4b5f2e5edfc796082a353fd09c1b1dec3e7a108a4b080fe3a86b12d9fa59fae6" } diff --git a/packages/app-bundle/overlay/packages/app/src/components/concise-mode-toggle.tsx b/packages/app-bundle/overlay/packages/app/src/components/concise-mode-toggle.tsx new file mode 100644 index 000000000..ed604f0ea --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/concise-mode-toggle.tsx @@ -0,0 +1,21 @@ +// amicode: concise-mode toggle for the v2 composer's bottom control row. +// A real switch (not an icon) — flipping it does NOT send a message; it sets a +// persistent per-session flag that, while on, attaches a concise directive to +// the NEXT message you send (as the invisible per-turn system instruction), so +// the response comes back concise. Turning it off stops that. The switch's own +// accent/checked styling (switch-v2.css) carries the on state; the "Concise" +// label is self-explanatory, so there is no tooltip. +import { Switch } from "@opencode-ai/ui/v2/switch-v2" + +export function ConciseModeToggle(props: { active: boolean; disabled?: boolean; onToggle: (next: boolean) => void }) { + return ( + props.onToggle(next)} + > + Concise + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx index ad9cc7ecb..e311ff18b 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx @@ -7,9 +7,12 @@ import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" import { createEffect, createMemo, on, onCleanup, Show } from "solid-js" +import { useSearchParams } from "@solidjs/router" +import { bindConcise, conciseKey, conciseMode } from "@/context/concise-mode" import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model" import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" import { ReportBugButton } from "@/components/report-bug-button" +import { ConciseModeToggle } from "@/components/concise-mode-toggle" import type { PromptInputProps } from "@/components/prompt-input/contracts" import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history" import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store" @@ -48,6 +51,10 @@ export type PromptInputV2ComposerProps = { export type PromptInputV2ControllerProps = Omit export type PromptInputV2ComposerController = PromptInputV2Interaction & { readonly model: PromptInputProps["controls"]["model"] + // amicode: the concise-mode toggle state + setter, surfaced to the composer's + // trailing controls. `on` is a reactive accessor; `set` flips a persistent + // per-session flag — it sends nothing. The flag is read at submit time. + readonly concise: { readonly on: () => boolean; readonly set: (next: boolean) => void } } export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { @@ -56,7 +63,7 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { const language = useLanguage() return ( -
+
: undefined} + trailingControl={ + <> +
+ props.controller.concise.set(next)} + /> +
+ {bugReportEnabled() ? : undefined} + + } modelControl={ () + const conciseKeyNow = () => { + const id = props.controls.session.id + return id + ? conciseKey.session(sdk().scope, sdk().directory, id) + : conciseKey.draft(sdk().scope, sdk().directory, conciseSearch.draftId) + } + const concise = bindConcise(conciseMode, conciseKeyNow) + const conciseOn = concise.on const submission = createPromptSubmit({ prompt, info, @@ -241,6 +273,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): onSubmit: props.onSubmit, streamGap, model: props.controls.model.selection, + concise: conciseOn, }) const referenceDescription = (reference: ReferenceInfo) => @@ -437,6 +470,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): }, }) Object.defineProperty(controller, "model", { get: () => props.controls.model }) + Object.defineProperty(controller, "concise", { get: () => concise }) // Framed webview: the window-level fallback (global-clipboard.ts) is the // sole ⌘V owner. When the clipboard carries no text it offers the media to diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit.ts index a6d6843f6..b2877efbd 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit.ts @@ -20,6 +20,7 @@ import { buildRequestParts } from "./build-request-parts" import { setCursorPosition } from "./editor-dom" import { formatServerError } from "@/utils/server-errors" import { ScopedKey } from "@/utils/server-scope" +import { conciseKey, conciseMode } from "@/context/concise-mode" import { createPromptSubmissionState } from "./submission-state" import { normalizeSessionInfo } from "@/utils/session" import { Event } from "@opencode-ai/schema/event" @@ -40,6 +41,9 @@ export type FollowupDraft = { agent: string model: { providerID: string; modelID: string } variant?: string + // amicode: per-turn system instruction (e.g. the concise directive). Reaches + // the model for this turn but is NOT rendered as a visible message part. + system?: string } type FollowupSendInput = { @@ -56,6 +60,12 @@ const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image") +// amicode: the concise directive. When the composer's concise switch is on it +// rides as the message's per-turn `system` instruction (invisible — reaches the +// model, not rendered as a user part), never appended to the visible text. +const CONCISE_DIRECTIVE = + "Concise mode is on for this message. Respond concisely: lead with the answer or next action, no preamble or closing pleasantries, and keep it tight — without dropping required caveats, questions, or safety confirmations." + export async function sendFollowupDraft(input: FollowupSendInput) { const text = draftText(input.draft.prompt) const images = draftImages(input.draft.prompt) @@ -135,6 +145,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { time: { created: Date.now() }, agent: input.draft.agent, model: { ...input.draft.model, variant: input.draft.variant }, + system: input.draft.system, } const add = () => @@ -172,6 +183,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { agent: input.draft.agent, model: input.draft.model, variant: input.draft.variant, + system: input.draft.system, legacyParts: requestParts, text: requestParts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"), files: requestParts.flatMap((part) => { @@ -234,6 +246,12 @@ type PromptSubmitInput = { * notice instead of optimistically posting into a dead tunnel. */ streamGap?: Accessor model?: ModelSelection + /** amicode: when true at send time, the concise directive rides as the + * message's per-turn `system` instruction (normal prose turns only) so the + * response comes back concise — the composer's concise switch drives this. + * #1651: the switch is session-keyed; a new chat's value is promoted to the + * created session below. */ + concise?: Accessor } export function createPromptSubmit(input: PromptSubmitInput) { @@ -372,6 +390,10 @@ export function createPromptSubmit(input: PromptSubmitInput) { const permissionState = permission.currentServerState() const isNewSession = !params.id const shouldAutoAccept = isNewSession && input.autoAccept() + // amicode#1651: read the concise switch (and the new chat's draft key) NOW, + // before any await — once the session exists the composer's key moves to it. + const conciseAtSend = input.concise?.() ?? false + const conciseDraftKey = conciseKey.draft(sdk().scope, projectDirectory, search.draftId) const worktreeSelection = input.newSessionWorktree?.() || "main" let sessionDirectory = projectDirectory @@ -444,6 +466,8 @@ export function createPromptSubmit(input: PromptSubmitInput) { model: { providerID: currentModel.provider.id, modelID: currentModel.id }, variant: variant ?? null, }) + // amicode#1651: the new chat's concise switch follows it into the session. + conciseMode.promote(conciseDraftKey, conciseKey.session(sdk().scope, sessionDirectory, session.id)) layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) const draftID = search.draftId if (draftID) tabs.promoteDraft(draftID, { server: tabs.draft(draftID).server, sessionId: session.id }) @@ -473,6 +497,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { agent, model, variant, + system: conciseAtSend ? CONCISE_DIRECTIVE : undefined, } const clearInput = () => { diff --git a/packages/app-bundle/overlay/packages/app/src/context/concise-mode.test.ts b/packages/app-bundle/overlay/packages/app/src/context/concise-mode.test.ts new file mode 100644 index 000000000..0c74718ff --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/context/concise-mode.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "bun:test" +import { bindConcise, conciseKey, createConciseStore } from "./concise-mode" +import { ServerScope } from "@/utils/server-scope" + +// amicode#1651 — the concise switch is per-SESSION state, not per-composer- +// instance state. These pin the store + the binding a composer controller uses, +// so a remount (new chat → session, Changes tab, session switch) cannot reset it. + +const scope = ServerScope.local +const dir = "/repo" + +describe("concise store (#1651)", () => { + test("an unknown key reads as off", () => { + const store = createConciseStore() + expect(store.get(conciseKey.session(scope, dir, "s1"))).toBe(false) + }) + + test("returns what was set, keys are independent", () => { + const store = createConciseStore() + const a = conciseKey.session(scope, dir, "a") + const b = conciseKey.session(scope, dir, "b") + store.set(a, true) + expect(store.get(a)).toBe(true) + expect(store.get(b)).toBe(false) + store.set(a, false) + expect(store.get(a)).toBe(false) + }) + + test("promote carries the draft value to the session and clears the draft", () => { + const store = createConciseStore() + const draft = conciseKey.draft(scope, dir, "d1") + const session = conciseKey.session(scope, dir, "s1") + store.set(draft, true) + store.promote(draft, session) + expect(store.get(session)).toBe(true) + expect(store.get(draft)).toBe(false) + }) + + test("promote from an unset draft leaves the session untouched", () => { + const store = createConciseStore() + const session = conciseKey.session(scope, dir, "s1") + store.set(session, true) + store.promote(conciseKey.draft(scope, dir, "d-none"), session) + expect(store.get(session)).toBe(true) + }) + + test("draft and session keys never collide", () => { + expect(conciseKey.draft(scope, dir, "x")).not.toBe(conciseKey.session(scope, dir, "x")) + expect(conciseKey.draft(scope, dir, undefined)).toBe(conciseKey.draft(scope, dir, undefined)) + }) +}) + +describe("concise binding — survives composer remount (#1651)", () => { + test("a second binding on the same key (a remount) sees the value set by the first", () => { + const store = createConciseStore() + const key = conciseKey.session(scope, dir, "s1") + const first = bindConcise(store, () => key) + first.set(true) + const remounted = bindConcise(store, () => key) + expect(remounted.on()).toBe(true) + }) + + test("new chat → session: the session-page binding reads the promoted value", () => { + const store = createConciseStore() + const draft = conciseKey.draft(scope, dir, "d1") + const session = conciseKey.session(scope, dir, "s1") + bindConcise(store, () => draft).set(true) + store.promote(draft, session) + expect(bindConcise(store, () => session).on()).toBe(true) + }) + + test("session creation failed (no promote): the draft keeps its value for the retry", () => { + const store = createConciseStore() + const draft = conciseKey.draft(scope, dir, "d1") + bindConcise(store, () => draft).set(true) + expect(bindConcise(store, () => draft).on()).toBe(true) + }) + + test("the binding follows its key: switching sessions restores each value", () => { + const store = createConciseStore() + let current = conciseKey.session(scope, dir, "a") + const binding = bindConcise(store, () => current) + binding.set(true) + current = conciseKey.session(scope, dir, "b") + expect(binding.on()).toBe(false) + current = conciseKey.session(scope, dir, "a") + expect(binding.on()).toBe(true) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/context/concise-mode.ts b/packages/app-bundle/overlay/packages/app/src/context/concise-mode.ts new file mode 100644 index 000000000..7692cc3e5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/context/concise-mode.ts @@ -0,0 +1,49 @@ +// amicode#1651: the composer's concise switch, held PER SESSION (in memory). +// +// The switch used to be a createSignal inside the composer controller, i.e. +// per-component-instance state: every composer remount reset it to off — the +// new-chat composer being replaced by the session composer on first send, the +// Changes tab unmounting the composer in the narrow layout, a session switch. +// Here the flag lives in a module-level store keyed by session, so a remounted +// composer reads the value back. A new chat has no session id yet: it writes +// under a DRAFT key, which the submit path promotes to the real session key +// the moment the session is created (beside the agent/model promotion). +// +// In memory by design — it resets on app reload; there is no persistence layer. +import { createStore } from "solid-js/store" +import { ScopedKey, type ServerScope } from "@/utils/server-scope" + +export const conciseKey = { + session: (scope: ServerScope, directory: string, sessionID: string): string => + ScopedKey.from(scope, directory, "session", sessionID), + draft: (scope: ServerScope, directory: string, draftID: string | undefined): string => + ScopedKey.from(scope, directory, "draft", draftID ?? ""), +} + +export type ConciseStore = ReturnType + +export function createConciseStore() { + const [state, setState] = createStore>({}) + return { + get: (key: string) => state[key] === true, + set: (key: string, on: boolean) => setState(key, on), + /** Carry a draft's value to its newly created session and drop the draft entry. */ + promote: (from: string, to: string) => { + const value = state[from] + if (value === undefined) return + setState(to, value) + setState(from, undefined) + }, + } +} + +/** The app-wide instance the composer and the submit path share. */ +export const conciseMode = createConciseStore() + +/** A composer's view of the flag: always reads/writes the CURRENT key. */ +export function bindConcise(store: ConciseStore, key: () => string) { + return { + on: () => store.get(key()), + set: (next: boolean) => store.set(key(), next), + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/index.css b/packages/app-bundle/overlay/packages/app/src/index.css index 404975bb5..1a50b887c 100644 --- a/packages/app-bundle/overlay/packages/app/src/index.css +++ b/packages/app-bundle/overlay/packages/app/src/index.css @@ -414,3 +414,19 @@ .preview-tab-strip [data-slot="tabs-trigger"][data-selected] .tab-fileicon-mono { display: none !important; } + +/* amicode(#1633): responsive concise toggle. The composer wrapper is a named + query container (.amicode-composer-cq); hide the concise toggle when the + composer itself is narrow — the window narrowed OR a side panel / split + shrank it — so the control row never crowds the Send button. Default (wide) + is visible; hidden only below the breakpoint. Tune the max-width to taste. */ +.amicode-composer-cq { + container-type: inline-size; + container-name: amicode-composer; +} + +@container amicode-composer (max-width: 26rem) { + .amicode-concise-toggle-wrap { + display: none; + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts b/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts index b928d9715..23c4b9aaa 100644 --- a/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts +++ b/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts @@ -44,6 +44,10 @@ type LegacyPrompt = { agent?: string model?: { providerID: string; modelID: string } variant?: string + // amicode: per-turn system instruction (e.g. the composer's concise + // directive) — forwarded to promptAsync's `system`, seen by the model for + // this turn but not rendered as a visible message part. + system?: string legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] } type LegacyLocation = { directory?: string } @@ -218,6 +222,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi { agent: value.agent, model: value.model, variant: value.variant, + system: value.system, parts: value.legacyParts ?? [ { type: "text", text: value.text }, ...(value.files ?? []).map((file) => ({ diff --git a/packages/extension/skills/concise/SKILL.md b/packages/extension/skills/concise/SKILL.md new file mode 100644 index 000000000..2ec68fd91 --- /dev/null +++ b/packages/extension/skills/concise/SKILL.md @@ -0,0 +1,105 @@ +--- +name: concise +description: Toggle concise mode — shape every response to lead with the answer or next action, cut preamble and closers, number multi-step work, cap lists, and state errors matter-of-factly. Invoke ONLY when the user asks for concise/brief/terse output or says "concise mode"; it stays on for the rest of the session until they say "normal mode" or "stop concise". Never auto-trigger on ordinary turns. +agents: [] +surface: public +--- + +# concise + +Concise mode is on. Output is not just short — it is shaped so the reader can act +on it immediately: the answer first, the ceremony gone. + +Adapted from the MIT-licensed `i-have-adhd` skill by ayghri +(https://github.com/ayghri/i-have-adhd) for Amico's research-copilot context. + +## Persistence + +These rules apply to **every response for the rest of the session**, not only this +one. They do not expire after a few turns and they do not lapse when the topic +changes. If you are unsure whether they still apply, they do. + +Turn them off only when the reader says **"normal mode"** or **"stop concise"**. +Confirm in one line, then return to your default style. + +## Why this shape + +1. Working memory is small. Anything not on screen is forgotten — do not ask the reader to "keep in mind X." +2. Knowing the answer is not doing the answer. The gap between "got it" and "done it" is where work dies. +3. Starting is the hardest step. The first action must be obvious, small, and doable now. +4. Vague time estimates fail. "A bit of work" and "a few hours" register the same. +5. Buried wins do not register. Visible progress matters. + +## Rules + +### 1. Lead with the answer or next action +The first line is the answer, or something the reader can do — not context, not a plan. +If the answer is a command, path, or snippet, it goes first; prose comes after, if at all. + +Bad: "Let's think about this. Your solve has a few moving pieces…" +Good: "Pin the globals: set `fix_global_variable!(δ)` at `script.jl:42`, then re-run." + +### 2. Number multi-step tasks +More than one step → a numbered list, each step one bounded action, no step with "and then" twice. +Use the fewest steps that still work. A short path finished beats a complete path abandoned. + +### 3. End with one concrete next action +If anything is open, name ONE thing doable in under two minutes ("open the file" counts). + +Bad: "Hope that helps — let me know if you want to dig deeper." +Good: "Next: run the solve and paste the first `AMICODE_ITER` line." + +### 4. Suppress tangents +Finish the first issue, then offer the second as a separate question. A question that +comes up mid-work is not a tangent — answer it yourself if you can and fold it in; +if it still needs the reader, surface it once, at the end. + +### 5. Restate state every turn +The reader cannot hold "step 3 of 5" between messages. Restate it. If the harness has +a todo/plan tool, use it for multi-step work — one item in progress at a time — and let +the checklist do the restating instead of narrating the full plan as prose. + +### 6. Give specific time estimates, in concrete units +Bad: "This will take some work." Good: "~15 min if the template covers it; an afternoon if not." + +### 7. Make completed work visible +Show what now works, concretely. Do not bury the win in a recap. +Good: "Solve converged: `F = 0.9982` in 137 iterations. Pulse at `runs/…/pulse.jld2`." + +### 8. Matter-of-fact tone for errors +Never "Uh oh" / "Oh no" / "There seems to be a problem." State cause and fix. +Good: "Solve failed at `iter 12`: `PosDefException`. Cause: Hessian not PSD. Fix: lower the trust-region radius." + +### 9. Cap visible lists to 5 items +Group related items, rank the most relevant first, show at most five per group. Retain the +rest internally; surface them when asked or when they become the next thing to address. +This shapes **presentation only** — it must never limit analysis, tool results, search, +candidate generation, or retained information. + +### 10. No preamble, no recap, no closing pleasantries +Forbidden openers: "Great question," "Let me…," "I'll…," "Sure!," "Looking at your…," "To answer your question…" +Forbidden recaps: "I've now done X, Y, and Z, which means…" +Forbidden closers: "Let me know if you need anything else," "Hope this helps," "Feel free to ask." +Start with the answer. End when the answer is done. + +## When to break the rules + +Override the defaults when: + +1. **The reader asks to "explain" or "walk me through."** Explain fully — still no preamble/closer, but the body runs as long as the topic needs. Add headers so they can skim back. +2. **A destructive or irreversible action is ahead** (`rm -rf`, force push, a paid hardware/QPU submit, a schema migration, promotion to a shared vault/catalog). Confirm before acting — safety and the human gate win over brevity. +3. **Debug spiral.** If the last three turns are "still broken," stop iterating on code: name the assumption that might be wrong and ask one diagnostic question. +4. **Real ambiguity.** One short clarifying question beats guessing and rewriting. +5. **A rule fights the task.** When a rule would delete the answer itself, the task wins, the shape stays. "What are my options?" gets 2–4 ranked options with one-line trade-offs, recommendation first — the options are the answer. +6. **A rule fights the harness or the persona.** Amico's own contract outranks this skill: keep interview questions one-per-turn, announce tool calls when the harness requires it, honor every gate and verification step, and never let brevity suppress a caveat, an "untrusted until verified" flag, or a required confirmation. A `free`-tier fidelity is still untrusted and you still say so. + +## Pre-send check + +Before sending, delete: +1. The first sentence if it announces what you are about to do. +2. The last sentence if it asks "anything else?" or recaps what just happened. +3. Any "by the way" sidebar. +4. Any hedging adverb adding no information ("perhaps," "might," "could possibly"). Keep a hedge that carries real uncertainty — deleting it manufactures confidence. +5. Any idiom or figurative phrase ("circle back," "on the same page"). Replace with the literal action. + +Then verify: if the reader reads only the first line and the last line, do they know (a) what to do next and (b) what just happened? If yes, send.