Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions openspec/changes/new-task-defaults/change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Change: New Task Defaults for Steps, Skip-Permissions, and Propagate

## Summary

Add three persistent app-level defaults that pre-fill checkboxes in the New
Task dialog, so users who habitually enable the same options do not have to
re-tick them on every task.

## Affected spec

`openspec/specs/steps-tracking/spec.md` — the `showSteps` field referenced in
the "opt-in per task with remembered default" scenarios is renamed to
`defaultStepsEnabled`. Behaviour is unchanged; only the storage key differs.
Existing `showSteps` values are migrated to `defaultStepsEnabled` on first
load.

## New behaviour

### `defaultStepsEnabled` (replaces `showSteps`)

- **WHEN** the user opens the new-task dialog
- **THEN** the "Steps tracking" checkbox is pre-checked iff `defaultStepsEnabled` is true
- **WHEN** the user toggles the Setting in Settings → General → New Task Defaults
- **THEN** `defaultStepsEnabled` is updated; task creation does not mutate this default

### `defaultSkipPermissions`

- **WHEN** the user opens the new-task dialog and the selected agent supports skip-permissions
- **THEN** the "Skip permissions" checkbox is pre-checked iff `defaultSkipPermissions` is true

### `defaultPropagateSkipPermissions`

- **WHEN** the user opens the new-task dialog with coordinator mode enabled and skip-permissions ticked
- **THEN** the "Propagate skip-permissions to sub-tasks" checkbox is pre-checked iff
`defaultPropagateSkipPermissions` is true

## Settings surface

All three defaults are exposed in **Settings → General → New Task Defaults**
(adjacent to the Behavior section). The propagate row is only shown when
`coordinatorModeEnabled` is true.

## Migration

`loadState` maps `raw.showSteps === true` → `defaultStepsEnabled = true` so
existing users who had steps tracking enabled keep their preference.
`showSteps` is no longer written by `saveState`.
30 changes: 30 additions & 0 deletions openspec/changes/new-task-defaults/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# New Task Defaults for Steps, Skip-Permissions, and Propagate

## Why

Users who habitually enable the same options (steps tracking, skip-permissions,
propagate to sub-tasks) must re-tick them on every new task. There is no way to
set an app-level default so the dialog opens pre-checked.

## What changes

- Three new boolean settings in **Settings → General → New Task Defaults**:
- **Steps tracking** — pre-tick "Steps tracking" in the New Task dialog
- **Skip permissions** — pre-tick "Skip permissions" (only honoured when the
selected agent supports it)
- **Propagate skip-permissions to sub-tasks** — pre-tick propagate (shown
only when `coordinatorModeEnabled` is true)
- All three values are persisted and included in the autosave snapshot.
- The `showSteps` key is renamed to `defaultStepsEnabled`; existing users'
preferences are migrated transparently on first load.

## Impact

- Modifies `openspec/specs/steps-tracking/spec.md`: renames `showSteps` →
`defaultStepsEnabled` and documents the two new fields.
- Store: adds `defaultSkipPermissions`, `defaultPropagateSkipPermissions`;
renames `showSteps` → `defaultStepsEnabled` in `AppStore`, `PersistedState`,
`saveState`, and `persistedSnapshot`.
- New Task dialog: open effect initializes all three signals from store instead
of hardcoding `false`.
- No new IPC channels.
63 changes: 63 additions & 0 deletions openspec/changes/new-task-defaults/specs/steps-tracking/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## MODIFIED Requirements

### Requirement: Opt-in per task with remembered default

Steps tracking SHALL be opt-in on a per-task basis, with a persistent app-level
default that the new-task dialog uses to pre-fill the checkbox. The storage key
for this default is renamed from `showSteps` to `defaultStepsEnabled`; existing
`showSteps` values are migrated to `defaultStepsEnabled` on first load.

#### Scenario: New task dialog reflects the persistent default

- **WHEN** the user opens the new-task dialog
- **THEN** the "Steps tracking" checkbox is pre-checked if and only if the
persisted `defaultStepsEnabled` app-level flag is true

#### Scenario: Settings toggle updates the default

- **WHEN** the user enables or disables "Steps tracking" in Settings → General → New Task Defaults
- **THEN** the persisted `defaultStepsEnabled` app-level flag is updated to match
- **AND** the next new-task dialog uses that value as the default

#### Scenario: Task creation does not update the default

- **WHEN** the user creates a task with the "Steps tracking" checkbox in a
different state than `defaultStepsEnabled`
- **THEN** the persisted `defaultStepsEnabled` app-level flag is NOT changed
- **AND** the per-task `stepsEnabled` flag reflects the checkbox state at creation time

#### Scenario: Migration from legacy showSteps key

- **WHEN** a user with `showSteps: true` in saved state opens the app after
this change
- **THEN** `defaultStepsEnabled` is set to true
- **AND** `showSteps` is no longer written by saveState

## ADDED Requirements

### Requirement: Persistent defaults for skip-permissions and propagate

Two additional app-level defaults SHALL be persisted and used to pre-fill the
New Task dialog checkboxes.

#### Scenario: Skip-permissions default pre-fills the dialog

- **WHEN** the user opens the new-task dialog and the selected agent supports
skip-permissions
- **THEN** the "Skip permissions" checkbox is pre-checked iff
`defaultSkipPermissions` is true

#### Scenario: Propagate default pre-fills the dialog

- **WHEN** the user opens the new-task dialog with coordinator mode enabled and
skip-permissions ticked
- **THEN** the "Propagate skip-permissions to sub-tasks" checkbox is pre-checked
iff `defaultPropagateSkipPermissions` is true

#### Scenario: Defaults are configurable in Settings

- **WHEN** the user opens Settings → General
- **THEN** a "New Task Defaults" section is visible with toggles for all three
defaults
- **AND** the propagate toggle is shown only when `coordinatorModeEnabled` is
true
22 changes: 22 additions & 0 deletions openspec/changes/new-task-defaults/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Tasks — New Task Defaults

- [x] Add `defaultStepsEnabled`, `defaultSkipPermissions`, and
`defaultPropagateSkipPermissions` to `AppStore` and `PersistedState` in
`src/store/types.ts`; remove `showSteps` from `PersistedState`.
- [x] Add setters for all three fields in `src/store/ui.ts`.
- [x] Update `saveState` in `src/store/persistence.ts` to write the three new
fields and stop writing `showSteps`.
- [x] Add migration in `loadState`: use `raw.defaultStepsEnabled` when it is a boolean; fall back to `raw.showSteps === true` only when the new field is absent (not present-but-invalid).
- [x] Add all three fields to `persistedSnapshot()` in `src/store/autosave.ts`
and export the function for testing.
- [x] Fix New Task dialog open effect (`src/components/NewTaskDialog.tsx`) to
initialize all three signals from the store rather than hardcoding `false`.
- [x] Add the three Settings rows in `src/components/SettingsDialog.tsx`
(New Task Defaults section, placed after the Behavior group).
- [x] Add autosave regression tests (`src/store/autosave.test.ts`) using the
real `persistedSnapshot()`.
- [x] Add migration tests to `src/store/persistence.test.ts`.
- [x] Update `openspec/specs/steps-tracking/spec.md` to rename `showSteps` →
`defaultStepsEnabled` and document the two new fields.
- [x] Validate with `npm run typecheck`, `npm test`, and
`openspec validate --all --strict`.
17 changes: 12 additions & 5 deletions openspec/specs/steps-tracking/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ reading through raw terminal scrollback.
Steps tracking SHALL be opt-in on a per-task basis, with a persistent app-level
default that the new-task dialog uses to pre-fill the checkbox.

#### Scenario: New task dialog reflects the last-used default
#### Scenario: New task dialog reflects the persistent default

- **WHEN** the user opens the new-task dialog
- **THEN** the "Steps tracking" checkbox is pre-checked if and only if the
persisted `showSteps` app-level flag is true
persisted `defaultStepsEnabled` app-level flag is true

#### Scenario: Default updates when user toggles the checkbox
#### Scenario: Settings toggle updates the default

- **WHEN** the user creates a task with the checkbox in the opposite state
- **THEN** the persisted `showSteps` app-level flag is updated to match
- **WHEN** the user enables or disables "Steps tracking" in Settings → General → New Task Defaults
- **THEN** the persisted `defaultStepsEnabled` app-level flag is updated to match
- **AND** the next new-task dialog uses that value as the default

#### Scenario: Task creation does not update the default

- **WHEN** the user creates a task with the "Steps tracking" checkbox in a
different state than `defaultStepsEnabled`
- **THEN** the persisted `defaultStepsEnabled` app-level flag is NOT changed
- **AND** the per-task `stepsEnabled` flag reflects the checkbox state at creation time

#### Scenario: `stepsEnabled` is per-task and persisted

- **WHEN** a task is created with the checkbox enabled
Expand Down
38 changes: 31 additions & 7 deletions src/components/NewTaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Show,
onCleanup,
on,
untrack,
} from 'solid-js';
import { Dialog } from './Dialog';
import { errMessage } from '../lib/log';
Expand Down Expand Up @@ -75,8 +76,8 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
const [branchesError, setBranchesError] = createSignal(false);
// Bumped by the Retry button to re-run the branch-fetch effect.
const [branchRetryToken, setBranchRetryToken] = createSignal(0);
const [stepsEnabled, setStepsEnabled] = createSignal(store.showSteps);
const [skipPermissions, setSkipPermissions] = createSignal(false);
const [stepsEnabled, setStepsEnabled] = createSignal(store.defaultStepsEnabled);
const [skipPermissions, setSkipPermissions] = createSignal(store.defaultSkipPermissions);
const [dockerMode, setDockerMode] = createSignal(false);
const [dockerImageReady, setDockerImageReady] = createSignal<boolean | null>(null); // null = unknown
const [dockerBuilding, setDockerBuilding] = createSignal(false);
Expand All @@ -88,7 +89,9 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
buildContext: string;
} | null>(null);
const [coordinatorMode, setCoordinatorMode] = createSignal(false);
const [propagateSkipPermissions, setPropagateSkipPermissions] = createSignal(false);
const [propagateSkipPermissions, setPropagateSkipPermissions] = createSignal(
store.defaultPropagateSkipPermissions,
);
const [maxConcurrentTasks, setMaxConcurrentTasks] = createSignal(
DEFAULT_COORDINATOR_CONCURRENT_TASKS,
);
Expand Down Expand Up @@ -158,7 +161,27 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
focusables[nextIdx].focus();
}

// Initialize state each time the dialog opens
// Initialize state each time the dialog opens. Wrapped in on() so the
// effect only re-fires on the props.open *transition*, not whenever any
// store default mutates while the dialog is already open (e.g. the user
// toggling Settings, or autosave restoring state). untrack() ensures the
// store reads inside are one-shot samples, not new reactive subscriptions.
createEffect(
on(
() => props.open,
(open) => {
if (!open) return;
untrack(() => {
setStepsEnabled(store.defaultStepsEnabled);
setSkipPermissions(store.defaultSkipPermissions);
setPropagateSkipPermissions(store.defaultPropagateSkipPermissions);
});
},
{ defer: true },
),
);

// Initialize remaining state each time the dialog opens
createEffect(() => {
if (!props.open) return;

Expand All @@ -168,8 +191,6 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
setError('');
setLoading(false);
setGitIsolation('worktree');
setSkipPermissions(false);
setPropagateSkipPermissions(false);
setDockerMode(false);
setDockerImageReady(null);
setDockerBuilding(false);
Expand Down Expand Up @@ -629,7 +650,10 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
? (projDocker?.imageTag ?? (store.dockerImage || DEFAULT_DOCKER_IMAGE))
: undefined,
coordinatorMode: coordinatorMode() || undefined,
propagateSkipPermissions: coordinatorMode() ? propagateSkipPermissions() : undefined,
propagateSkipPermissions:
coordinatorMode() && agentSupportsSkipPermissions() && skipPermissions()
? propagateSkipPermissions()
: undefined,
maxConcurrentTasks: coordinatorMode()
? clampCoordinatorConcurrentTasks(maxConcurrentTasks())
: undefined,
Expand Down
90 changes: 90 additions & 0 deletions src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import {
setDarkTheme,
setCoordinatorModeEnabled,
setCoordinatorNotificationDelayMs,
setDefaultStepsEnabled,
setDefaultSkipPermissions,
setDefaultPropagateSkipPermissions,
updateStatus,
checkForUpdates,
} from '../store/store';
Expand Down Expand Up @@ -414,6 +417,93 @@ export function SettingsDialog(props: SettingsDialogProps) {
</label>
</div>

<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px' }}>
<div style={{ ...sectionLabelStyle, 'font-weight': '600' }}>New Task Defaults</div>
<label
style={{
display: 'flex',
'align-items': 'center',
gap: '10px',
cursor: 'pointer',
padding: '8px 12px',
'border-radius': '8px',
background: theme.bgInput,
border: `1px solid ${theme.border}`,
}}
>
<input
type="checkbox"
checked={store.defaultStepsEnabled}
onChange={(e) => setDefaultStepsEnabled(e.currentTarget.checked)}
style={{ 'accent-color': theme.accent, cursor: 'pointer' }}
/>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '2px' }}>
<span style={{ 'font-size': '14px', color: theme.fg }}>Steps tracking</span>
<span style={{ 'font-size': '12px', color: theme.fgSubtle }}>
Pre-tick Steps tracking in the New Task dialog
</span>
</div>
</label>
<label
style={{
display: 'flex',
'align-items': 'center',
gap: '10px',
cursor: 'pointer',
padding: '8px 12px',
'border-radius': '8px',
background: theme.bgInput,
border: `1px solid ${theme.border}`,
}}
>
<input
type="checkbox"
checked={store.defaultSkipPermissions}
onChange={(e) => setDefaultSkipPermissions(e.currentTarget.checked)}
style={{ 'accent-color': theme.accent, cursor: 'pointer' }}
/>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '2px' }}>
<span style={{ 'font-size': '14px', color: theme.fg }}>
Dangerously skip all confirms by default
</span>
<span style={{ 'font-size': '12px', color: theme.fgSubtle }}>
Pre-tick skip-permissions for every new task. The agent will run without asking
for confirmation. Only honoured when the selected agent supports it.
</span>
</div>
</label>
<Show when={store.coordinatorModeEnabled}>
<label
style={{
display: 'flex',
'align-items': 'center',
gap: '10px',
cursor: 'pointer',
padding: '8px 12px',
'border-radius': '8px',
background: theme.bgInput,
border: `1px solid ${theme.border}`,
}}
>
<input
type="checkbox"
checked={store.defaultPropagateSkipPermissions}
onChange={(e) => setDefaultPropagateSkipPermissions(e.currentTarget.checked)}
style={{ 'accent-color': theme.accent, cursor: 'pointer' }}
/>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '2px' }}>
<span style={{ 'font-size': '14px', color: theme.fg }}>
Propagate skip-permissions to sub-tasks
</span>
<span style={{ 'font-size': '12px', color: theme.fgSubtle }}>
Pre-tick Propagate to sub-tasks when both coordinator mode and skip-permissions
are enabled for a task
</span>
</div>
</label>
</Show>
</div>

<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px' }}>
<div
style={{
Expand Down
Loading
Loading