clide:// deep link handler — open files at a line (T-56)
clide://open?path=&line= translates (in parseArgv) to editor.open, so an OS scheme invocation routes through the existing CLI→IPC path into the running window — single-instance for free, no new code path. Registered the scheme: linux/clide.desktop MimeType x-scheme-handler/clide (Exec already passes %U) and macOS Info.plist CFBundleURLTypes. Parser validates the action/path/line. Linux works end to end (the OS passes the URL as argv). macOS URL DELIVERY (the AppDelegate openURLs callback → forward into this path) is a follow-up — the scheme is registered, but the native hook needs a real macOS machine to verify, so it's not shipped blind. Also drops a stray import in clide_markdown_test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,12 @@ ArgvParseResult parseArgv(List<String> 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<String> argv, {required String requestId}) {
|
||||
));
|
||||
}
|
||||
|
||||
/// Translate a `clide://` deep link into an [IpcRequest] (T-56). Only the
|
||||
/// `open` action is defined today: `clide://open?path=<abs|repo-rel>&line=<n>`
|
||||
/// 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 = <String>[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 {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -28,5 +28,16 @@
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>net.schweitz.clide.deeplink</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>clide</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user