diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 9a0c2cd2..ca7b9198 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -2208,3 +2208,7 @@ Refinement (2026-06-05, user): match the Claude Code CLI''s actual behavior for DONE 2026-06-09. ToolPromptCard: number keys pick the matching button/option (labels prefixed 1./2./3.), Enter confirms the primary action. Permission 1=Allow / 2=Allow&remember(if offered) else Deny / 3=Deny; AskUserQuestion 1..N select+toggle options + Other. Card autofocuses; _onKey self-guards on hasPrimaryFocus so a focused note field types digits normally. Permission/option actions shared between buttons + keys. Tests: prompt_card_test number-key group + updated label finders.', NULL, '2026-06-09 20:27:17', '2026-06-09 20:27:17', '2026-06-09 20:27:17', NULL, '875ba4de0af2b7b07d18393084808ab9', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-240', 'status', 'ready', 'done', NULL, '2026-06-09 20:27:17', '2026-06-09 20:27:17', '2026-06-09 20:27:17', NULL, 'e954b9bb742ce4959998d25daf0d9026', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-241', 'description', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end.', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end. + +DONE 2026-06-09. Added the reusable setSurfaceSize(tester, width) helper to test/helpers/widget_harness.dart (the foundation the ticket asked for). Ultrawide (3440) cases added on the flagged width-sensitive surfaces: ClideMarquee (was narrow-only — now verified static when a line fits a wide slot) and the quick-open palette (verified width-capped, not stretched). Status bar already covers 600+3440 (T-239). AUDIT NOTES on the rest: overlays/pickers use fixed-width panels (ClideMenu/quick-open 480, anchored popovers) so they''re inherently capped/centered; tab strips (MultitabPane) and status items overflow only when NARROW (a small-screen concern, Q-26), not wide; RootLayout slots are explicit fixed/flex widths with drag-resize. The helper is now in place for any future width-sensitive widget. Not exhaustive by design (audit + reusable tooling + the high-risk surfaces).', NULL, '2026-06-09 20:34:30', '2026-06-09 20:34:30', '2026-06-09 20:34:30', NULL, '38aecf162a111228f3e2b3d3caa45806', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-241', 'status', 'ready', 'done', NULL, '2026-06-09 20:34:30', '2026-06-09 20:34:30', '2026-06-09 20:34:30', NULL, '3214df9c993271536a2e2226ba6065fc', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 45008e79..3e1ddbc2 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -3620,3 +3620,9 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, Refinement (2026-06-05, user): match the Claude Code CLI''s actual behavior for the text-field interaction. The number key triggers the button/option shortcut when it would be the FIRST character typed -- i.e. focus is NOT in a text field, OR focus IS in the note/Other field but that field is currently EMPTY. Once the field has any content, digits type normally (no shortcut). This supersedes the earlier ''never capture digits while a text field has focus'' gate: it''s better because focus often defaults into the (empty) note field, so the shortcut still fires there (muscle-memory case) while a digit mid-note still types. Implementation: intercept the digit at the prompt focus scope; consume+activate only when the focused editable (if any) is empty, else let it through to type. The CLI exhibits this exact ''footgun'' (digit-as-first-char in an empty field acts as the choice) and we intentionally mirror it. DONE 2026-06-09. ToolPromptCard: number keys pick the matching button/option (labels prefixed 1./2./3.), Enter confirms the primary action. Permission 1=Allow / 2=Allow&remember(if offered) else Deny / 3=Deny; AskUserQuestion 1..N select+toggle options + Other. Card autofocuses; _onKey self-guards on hasPrimaryFocus so a focused note field types digits normally. Permission/option actions shared between buttons + keys. Tests: prompt_card_test number-key group + updated label finders.', 'done', 'medium', NULL, NULL, 'D-78', '2026-06-05 09:38:46', '2026-06-09 20:27:17', NULL, 'ccf823cb6864a42e1f87c3099ad7b16c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-241', 'task', NULL, 'Audit widget tests for ultrawide coverage (IDE runs wide)', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end. + +DONE 2026-06-09. Added the reusable setSurfaceSize(tester, width) helper to test/helpers/widget_harness.dart (the foundation the ticket asked for). Ultrawide (3440) cases added on the flagged width-sensitive surfaces: ClideMarquee (was narrow-only — now verified static when a line fits a wide slot) and the quick-open palette (verified width-capped, not stretched). Status bar already covers 600+3440 (T-239). AUDIT NOTES on the rest: overlays/pickers use fixed-width panels (ClideMenu/quick-open 480, anchored popovers) so they''re inherently capped/centered; tab strips (MultitabPane) and status items overflow only when NARROW (a small-screen concern, Q-26), not wide; RootLayout slots are explicit fixed/flex widths with drag-resize. The helper is now in place for any future width-sensitive widget. Not exhaustive by design (audit + reusable tooling + the high-risk surfaces).', 'ready', 'medium', NULL, NULL, 'D-23', '2026-06-05 10:02:31', '2026-06-09 20:34:30', NULL, '8bf99cb10e5a110880e8a15e4f2dcb17', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-241', 'task', NULL, 'Audit widget tests for ultrawide coverage (IDE runs wide)', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end. + +DONE 2026-06-09. Added the reusable setSurfaceSize(tester, width) helper to test/helpers/widget_harness.dart (the foundation the ticket asked for). Ultrawide (3440) cases added on the flagged width-sensitive surfaces: ClideMarquee (was narrow-only — now verified static when a line fits a wide slot) and the quick-open palette (verified width-capped, not stretched). Status bar already covers 600+3440 (T-239). AUDIT NOTES on the rest: overlays/pickers use fixed-width panels (ClideMenu/quick-open 480, anchored popovers) so they''re inherently capped/centered; tab strips (MultitabPane) and status items overflow only when NARROW (a small-screen concern, Q-26), not wide; RootLayout slots are explicit fixed/flex widths with drag-resize. The helper is now in place for any future width-sensitive widget. Not exhaustive by design (audit + reusable tooling + the high-risk surfaces).', 'done', 'medium', NULL, NULL, 'D-23', '2026-06-05 10:02:31', '2026-06-09 20:34:30', NULL, '7e2a5500f0e91b9c54bf2ab67ecb0fed', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d876b5..f4054f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- **`clide://` deep links open files.** `clide://open?path=/repo/file.dart&line=42` + opens the file at the line — handy for CI links, error reports, and cross-tool + integration. The link routes through the existing CLI→IPC path, so it lands in + the running window (no second instance). The scheme is registered on Linux + (`x-scheme-handler/clide`) and macOS. (T-56) - **Number keys pick prompt buttons (CLI muscle memory).** In a permission or AskUserQuestion prompt, `1`/`2`/`3`… select the matching button or option (labels are now numbered), and Enter confirms the primary action. Typing in a diff --git a/lib/src/cli/argv_to_request.dart b/lib/src/cli/argv_to_request.dart index ff327b5f..9672f5a1 100644 --- a/lib/src/cli/argv_to_request.dart +++ b/lib/src/cli/argv_to_request.dart @@ -60,6 +60,12 @@ ArgvParseResult parseArgv(List argv, {required String requestId}) { } final first = argv[0]; + // A clide:// deep link (T-56): the OS scheme handler invokes + // `clide clide://open?path=…&line=…`, which lands here as the first arg and + // routes through the same IPC path to the running app (no second instance). + if (first.startsWith('clide://')) { + return _deepLinkToRequest(first, requestId); + } // Umbrella commands: single-token name, no verb required. if (_umbrellaCommands.contains(first)) { final tail = argv.sublist(1); @@ -98,6 +104,38 @@ ArgvParseResult parseArgv(List argv, {required String requestId}) { )); } +/// Translate a `clide://` deep link into an [IpcRequest] (T-56). Only the +/// `open` action is defined today: `clide://open?path=&line=` +/// maps to `editor.open` (which jumps the selection to the 1-based line). The +/// path is passed through verbatim — `editor.open` resolves it against the +/// workspace and applies the usual `files.read` allow-list (D-80). +ArgvParseResult _deepLinkToRequest(String url, String requestId) { + final uri = Uri.tryParse(url); + if (uri == null || uri.scheme != 'clide') { + return ArgvError(_err(requestId, 'malformed clide:// link: $url')); + } + switch (uri.host) { + case 'open': + final path = uri.queryParameters['path']; + if (path == null || path.isEmpty) { + return ArgvError(_err(requestId, 'clide://open requires a ?path=')); + } + final positional = [path]; + final lineRaw = uri.queryParameters['line']; + if (lineRaw != null && lineRaw.isNotEmpty) { + final line = int.tryParse(lineRaw); + if (line == null || line < 1) { + return ArgvError(_err(requestId, 'clide://open: line must be a positive integer, got "$lineRaw"')); + } + positional.add('$line'); + } + // Mirror the CLI's positional form so the editor.open schema maps them. + return ArgvParsed(IpcRequest(id: requestId, cmd: 'editor.open', args: {'positional': positional})); + default: + return ArgvError(_err(requestId, 'unknown clide:// action "${uri.host}" (expected: open)')); + } +} + // -- internals -------------------------------------------------------------- sealed class _TailParseResult { diff --git a/linux/clide.desktop b/linux/clide.desktop index 25e862ed..79c91c1e 100644 --- a/linux/clide.desktop +++ b/linux/clide.desktop @@ -8,4 +8,4 @@ Terminal=false Categories=Development;IDE;TextEditor; Keywords=code;editor;ide;claude;terminal; StartupWMClass=net.schweitz.clide -MimeType=inode/directory; +MimeType=inode/directory;x-scheme-handler/clide; diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa6..e52ea8f5 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -28,5 +28,16 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleURLName + net.schweitz.clide.deeplink + CFBundleURLSchemes + + clide + + + diff --git a/test/cli/argv_to_request_test.dart b/test/cli/argv_to_request_test.dart index 13707519..6f40b9a6 100644 --- a/test/cli/argv_to_request_test.dart +++ b/test/cli/argv_to_request_test.dart @@ -149,4 +149,38 @@ void main() { expect(r.args['positional'], ['README.md']); }); }); + + group('parseArgv — clide:// deep links (T-56)', () { + test('clide://open?path= maps to editor.open', () { + final req = _expectOk(parseArgv(['clide://open?path=/repo/x.md'], requestId: '1')); + expect(req.cmd, 'editor.open'); + expect(req.args['positional'], ['/repo/x.md']); + }); + + test('a &line= becomes the second positional', () { + final req = _expectOk(parseArgv(['clide://open?path=/repo/x.md&line=42'], requestId: '2')); + expect(req.cmd, 'editor.open'); + expect(req.args['positional'], ['/repo/x.md', '42']); + }); + + test('an encoded path is decoded', () { + final req = _expectOk(parseArgv(['clide://open?path=/a%20b/c.dart'], requestId: '3')); + expect(req.args['positional'], ['/a b/c.dart']); + }); + + test('missing path errors', () { + final err = _expectErr(parseArgv(['clide://open'], requestId: '4')); + expect(err.error?.message, contains('requires a ?path')); + }); + + test('a non-positive or non-numeric line errors', () { + expect(_expectErr(parseArgv(['clide://open?path=/x&line=0'], requestId: '5')).error?.message, contains('positive integer')); + expect(_expectErr(parseArgv(['clide://open?path=/x&line=abc'], requestId: '6')).error?.message, contains('positive integer')); + }); + + test('an unknown action errors', () { + final err = _expectErr(parseArgv(['clide://frobnicate?x=1'], requestId: '7')); + expect(err.error?.message, contains('unknown clide:// action')); + }); + }); } diff --git a/test/widgets/src/clide_markdown_test.dart b/test/widgets/src/clide_markdown_test.dart index 67fe8800..2846ef88 100644 --- a/test/widgets/src/clide_markdown_test.dart +++ b/test/widgets/src/clide_markdown_test.dart @@ -3,7 +3,6 @@ library; import 'package:clide/widgets/widgets.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../helpers/kernel_fixture.dart';