: 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.