@@ -109,7 +109,13 @@ class _TabRow extends StatelessWidget {
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i = 0; i < sessions.length; i++) _Tab(session: sessions[i], active: i == activeIndex, tokens: tokens, onTap: () => onSelect(i), onClose: sessions[i].isPrimary ? null : () => onClose(i)),
|
||||
for (var i = 0; i < sessions.length; i++)
|
||||
_Tab(
|
||||
session: sessions[i],
|
||||
active: i == activeIndex,
|
||||
tokens: tokens,
|
||||
onTap: () => onSelect(i),
|
||||
onClose: sessions[i].isPrimary ? null : () => onClose(i)),
|
||||
const SizedBox(width: 4),
|
||||
_AddButton(tokens: tokens, onTap: onAdd),
|
||||
const Spacer(),
|
||||
|
||||
@@ -30,6 +30,5 @@ class DecisionTypeColors {
|
||||
rejected: Color(0xFFC03030),
|
||||
);
|
||||
|
||||
static DecisionTypeColors forTheme({required bool dark}) =>
|
||||
dark ? DecisionTypeColors.dark : DecisionTypeColors.light;
|
||||
static DecisionTypeColors forTheme({required bool dark}) => dark ? DecisionTypeColors.dark : DecisionTypeColors.light;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,10 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
if (_focusSub == null) {
|
||||
final kernel = ClideKernel.of(context);
|
||||
_focusSub = kernel.messages.subscribe(publisher: 'builtin.decisions', channel: 'focus').listen(_onFocus);
|
||||
_fileSub = kernel.events.on<DaemonEvent>().where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? '')).listen((_) => _refresh());
|
||||
_fileSub = kernel.events
|
||||
.on<DaemonEvent>()
|
||||
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? ''))
|
||||
.listen((_) => _refresh());
|
||||
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
|
||||
}
|
||||
if (!_loading || _decisions.isNotEmpty) return;
|
||||
@@ -43,7 +46,10 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (!mounted) return;
|
||||
if (_refreshing) { _pendingRefresh = true; return; }
|
||||
if (_refreshing) {
|
||||
_pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
_refreshing = true;
|
||||
_pendingRefresh = false;
|
||||
await _load();
|
||||
@@ -117,11 +123,20 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
|
||||
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||
if (_decisions.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
||||
if (_decisions.isEmpty)
|
||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
final hasFilter = lf.isNotEmpty;
|
||||
final filtered = hasFilter ? _decisions.where((d) => d.id.toLowerCase().contains(lf) || d.title.toLowerCase().contains(lf) || (d.domain ?? '').toLowerCase().contains(lf) || (d.type ?? '').contains(lf)).toList() : _decisions;
|
||||
final filtered = hasFilter
|
||||
? _decisions
|
||||
.where((d) =>
|
||||
d.id.toLowerCase().contains(lf) ||
|
||||
d.title.toLowerCase().contains(lf) ||
|
||||
(d.domain ?? '').toLowerCase().contains(lf) ||
|
||||
(d.type ?? '').contains(lf))
|
||||
.toList()
|
||||
: _decisions;
|
||||
|
||||
final confirmed = filtered.where((d) => d.type == 'confirmed').toList();
|
||||
final questions = filtered.where((d) => d.type == 'question').toList();
|
||||
@@ -140,7 +155,8 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
child: ClideTappable(
|
||||
onTap: _refreshing ? null : _refresh,
|
||||
tooltip: 'Refresh decisions',
|
||||
builder: (ctx, hovered, _) => ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
builder: (ctx, hovered, _) =>
|
||||
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -151,27 +167,45 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (confirmed.isNotEmpty) ClideAccordion(
|
||||
label: 'CONFIRMED', count: confirmed.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.confirmed, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('confirmed'),
|
||||
onToggle: () => _toggleSection('confirmed'),
|
||||
children: [for (final d in confirmed) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)],
|
||||
),
|
||||
if (questions.isNotEmpty) ClideAccordion(
|
||||
label: 'QUESTIONS', count: questions.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.question, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('question'),
|
||||
onToggle: () => _toggleSection('question'),
|
||||
children: [for (final d in questions) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)],
|
||||
),
|
||||
if (rejected.isNotEmpty) ClideAccordion(
|
||||
label: 'REJECTED', count: rejected.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.rejected, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('rejected'),
|
||||
onToggle: () => _toggleSection('rejected'),
|
||||
children: [for (final d in rejected) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)],
|
||||
),
|
||||
if (confirmed.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'CONFIRMED',
|
||||
count: confirmed.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.confirmed, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('confirmed'),
|
||||
onToggle: () => _toggleSection('confirmed'),
|
||||
children: [
|
||||
for (final d in confirmed)
|
||||
_DecisionCard(
|
||||
entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
|
||||
],
|
||||
),
|
||||
if (questions.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'QUESTIONS',
|
||||
count: questions.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.question, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('question'),
|
||||
onToggle: () => _toggleSection('question'),
|
||||
children: [
|
||||
for (final d in questions)
|
||||
_DecisionCard(
|
||||
entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
|
||||
],
|
||||
),
|
||||
if (rejected.isNotEmpty)
|
||||
ClideAccordion(
|
||||
label: 'REJECTED',
|
||||
count: rejected.length,
|
||||
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.rejected, shape: BoxShape.circle)),
|
||||
expanded: hasFilter || _isSectionExpanded('rejected'),
|
||||
onToggle: () => _toggleSection('rejected'),
|
||||
children: [
|
||||
for (final d in rejected)
|
||||
_DecisionCard(
|
||||
entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -233,8 +267,7 @@ class _DecisionCard extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
const Spacer(),
|
||||
if (entry.domain != null)
|
||||
ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
if (entry.domain != null) ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@@ -68,9 +68,7 @@ class _DiffViewState extends State<DiffView> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
c.showStaged
|
||||
? 'No staged changes.'
|
||||
: 'No unstaged changes.',
|
||||
c.showStaged ? 'No staged changes.' : 'No unstaged changes.',
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
@@ -81,8 +79,7 @@ class _DiffViewState extends State<DiffView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final diff in c.diffs)
|
||||
_FileDiff(diff: diff, controller: c),
|
||||
for (final diff in c.diffs) _FileDiff(diff: diff, controller: c),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -118,9 +115,7 @@ class _DiffToolbar extends StatelessWidget {
|
||||
child: ClideText(
|
||||
'Unstaged',
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.showStaged
|
||||
? tokens.globalTextMuted
|
||||
: tokens.globalForeground,
|
||||
color: controller.showStaged ? tokens.globalTextMuted : tokens.globalForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -134,9 +129,7 @@ class _DiffToolbar extends StatelessWidget {
|
||||
child: ClideText(
|
||||
'Staged',
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.showStaged
|
||||
? tokens.globalForeground
|
||||
: tokens.globalTextMuted,
|
||||
color: controller.showStaged ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -188,12 +181,8 @@ class _FileDiff extends StatelessWidget {
|
||||
color: tokens.panelHeaderForeground,
|
||||
),
|
||||
),
|
||||
if (additions > 0)
|
||||
ClideText('+$additions ', fontSize: clideFontCaption,
|
||||
color: tokens.statusSuccess),
|
||||
if (removals > 0)
|
||||
ClideText('-$removals', fontSize: clideFontCaption,
|
||||
color: tokens.statusError),
|
||||
if (additions > 0) ClideText('+$additions ', fontSize: clideFontCaption, color: tokens.statusSuccess),
|
||||
if (removals > 0) ClideText('-$removals', fontSize: clideFontCaption, color: tokens.statusError),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,8 +17,7 @@ import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class EditorController extends ChangeNotifier {
|
||||
EditorController({required this.ipc, required DaemonBus events})
|
||||
: _events = events {
|
||||
EditorController({required this.ipc, required DaemonBus events}) : _events = events {
|
||||
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
|
||||
}
|
||||
|
||||
@@ -77,9 +76,7 @@ class EditorController extends ChangeNotifier {
|
||||
_activePath = r.data['path']! as String;
|
||||
_content = (r.data['content'] as String?) ?? '';
|
||||
final sel = r.data['selection'];
|
||||
_selection = sel is Map
|
||||
? Selection.fromJson(sel.cast<String, Object?>())
|
||||
: const Selection.collapsed(0);
|
||||
_selection = sel is Map ? Selection.fromJson(sel.cast<String, Object?>()) : const Selection.collapsed(0);
|
||||
_dirty = (r.data['dirty'] as bool?) ?? false;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -45,8 +45,7 @@ class _EditorViewState extends State<EditorView> {
|
||||
super.didChangeDependencies();
|
||||
if (_controller != null) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
_controller = EditorController(ipc: kernel.ipc, events: kernel.events)
|
||||
..addListener(_onControllerChanged);
|
||||
_controller = EditorController(ipc: kernel.ipc, events: kernel.events)..addListener(_onControllerChanged);
|
||||
unawaited(_controller!.hydrate());
|
||||
}
|
||||
|
||||
@@ -80,29 +79,22 @@ class _EditorViewState extends State<EditorView> {
|
||||
final c = _controller;
|
||||
if (c == null || c.activeId == null) return;
|
||||
final value = _text.value;
|
||||
if (value.text == c.content &&
|
||||
value.selection.baseOffset == c.selection.start &&
|
||||
value.selection.extentOffset == c.selection.end) {
|
||||
if (value.text == c.content && value.selection.baseOffset == c.selection.start && value.selection.extentOffset == c.selection.end) {
|
||||
return;
|
||||
}
|
||||
_lastRemoteContent = value.text;
|
||||
c.pushLocalEdit(
|
||||
newContent: value.text,
|
||||
newSelection: Selection(
|
||||
start: value.selection.start < 0
|
||||
? value.text.length
|
||||
: value.selection.start,
|
||||
end: value.selection.end < 0
|
||||
? value.text.length
|
||||
: value.selection.end,
|
||||
start: value.selection.start < 0 ? value.text.length : value.selection.start,
|
||||
end: value.selection.end < 0 ? value.text.length : value.selection.end,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, KeyEvent event) {
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
final isCmd = HardwareKeyboard.instance.isMetaPressed ||
|
||||
HardwareKeyboard.instance.isControlPressed;
|
||||
final isCmd = HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed;
|
||||
if (isCmd && event.logicalKey == LogicalKeyboardKey.keyS) {
|
||||
unawaited(_controller?.save());
|
||||
return KeyEventResult.handled;
|
||||
|
||||
@@ -8,8 +8,7 @@ import 'package:clide/kernel/src/theme/tokens.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SyntaxTextController extends TextEditingController {
|
||||
SyntaxTextController({required TreeSitterService syntax})
|
||||
: _syntax = syntax;
|
||||
SyntaxTextController({required TreeSitterService syntax}) : _syntax = syntax;
|
||||
|
||||
final TreeSitterService _syntax;
|
||||
|
||||
@@ -74,8 +73,7 @@ class SyntaxTextController extends TextEditingController {
|
||||
|
||||
// Convert byte offsets to character offsets.
|
||||
// Build a byte-to-char map only up to the max byte we need.
|
||||
final spans = _spans.where((s) => s.end <= sourceBytes.length).toList()
|
||||
..sort((a, b) => a.start != b.start ? a.start - b.start : a.end - b.end);
|
||||
final spans = _spans.where((s) => s.end <= sourceBytes.length).toList()..sort((a, b) => a.start != b.start ? a.start - b.start : a.end - b.end);
|
||||
|
||||
if (spans.isEmpty) {
|
||||
return TextSpan(text: text, style: style);
|
||||
@@ -121,9 +119,7 @@ class SyntaxTextController extends TextEditingController {
|
||||
continue;
|
||||
}
|
||||
final spanCharStart = byteToChar[span.start];
|
||||
final spanCharEnd = span.end <= maxByte
|
||||
? byteToChar[span.end]
|
||||
: source.length;
|
||||
final spanCharEnd = span.end <= maxByte ? byteToChar[span.end] : source.length;
|
||||
|
||||
if (spanCharStart < charPos) continue;
|
||||
|
||||
|
||||
@@ -136,8 +136,7 @@ class _Children extends StatelessWidget {
|
||||
controller: controller,
|
||||
depth: depth,
|
||||
),
|
||||
if (controller.isExpanded(e.path))
|
||||
_Children(path: e.path, controller: controller, depth: depth + 1),
|
||||
if (controller.isExpanded(e.path)) _Children(path: e.path, controller: controller, depth: depth + 1),
|
||||
],
|
||||
)
|
||||
else
|
||||
@@ -294,4 +293,3 @@ class _FilteredFileRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,94 +76,93 @@ class _GitPanelViewState extends State<GitPanelView> {
|
||||
child: Column(
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)),
|
||||
Expanded(child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_BranchHeader(controller: c),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
child: ClideText(
|
||||
c.error!,
|
||||
color: tokens.statusError,
|
||||
fontSize: clideFontCaption,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
if (c.loading && c.isClean)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Loading…', muted: true),
|
||||
),
|
||||
if (!c.loading && c.isClean && c.error == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Nothing to commit, working tree clean.',
|
||||
muted: true),
|
||||
),
|
||||
if (c.conflicted.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Merge conflicts',
|
||||
entries: _applyFilter(c.conflicted),
|
||||
actions: const [],
|
||||
),
|
||||
if (c.staged.isNotEmpty) ...[
|
||||
_FileGroup(
|
||||
label: 'Staged',
|
||||
entries: _applyFilter(c.staged),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Unstage all',
|
||||
onTap: () => unawaited(c.unstage(const [])),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_BranchHeader(controller: c),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(
|
||||
c.error!,
|
||||
color: tokens.statusError,
|
||||
fontSize: clideFontCaption,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
if (c.loading && c.isClean)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Loading…', muted: true),
|
||||
),
|
||||
if (!c.loading && c.isClean && c.error == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Nothing to commit, working tree clean.', muted: true),
|
||||
),
|
||||
if (c.conflicted.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Merge conflicts',
|
||||
entries: _applyFilter(c.conflicted),
|
||||
actions: const [],
|
||||
),
|
||||
if (c.staged.isNotEmpty) ...[
|
||||
_FileGroup(
|
||||
label: 'Staged',
|
||||
entries: _applyFilter(c.staged),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Unstage all',
|
||||
onTap: () => unawaited(c.unstage(const [])),
|
||||
),
|
||||
],
|
||||
onUnstage: (path) => unawaited(c.unstage([path])),
|
||||
),
|
||||
_CommitInput(
|
||||
commitMsg: _commitMsg,
|
||||
commitFocus: _commitFocus,
|
||||
controller: c,
|
||||
),
|
||||
],
|
||||
onUnstage: (path) => unawaited(c.unstage([path])),
|
||||
),
|
||||
_CommitInput(
|
||||
commitMsg: _commitMsg,
|
||||
commitFocus: _commitFocus,
|
||||
controller: c,
|
||||
),
|
||||
],
|
||||
if (c.unstaged.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Changes',
|
||||
entries: _applyFilter(c.unstaged),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Stage all',
|
||||
onTap: () => unawaited(c.stageAll()),
|
||||
if (c.unstaged.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Changes',
|
||||
entries: _applyFilter(c.unstaged),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Stage all',
|
||||
onTap: () => unawaited(c.stageAll()),
|
||||
),
|
||||
],
|
||||
onStage: (path) => unawaited(c.stage([path])),
|
||||
onDiscard: (path) => _confirmDiscard(context, c, path),
|
||||
),
|
||||
],
|
||||
onStage: (path) => unawaited(c.stage([path])),
|
||||
onDiscard: (path) => _confirmDiscard(context, c, path),
|
||||
),
|
||||
if (c.untracked.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Untracked',
|
||||
entries: _applyFilter(c.untracked),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Stage all',
|
||||
onTap: () {
|
||||
final paths = [
|
||||
for (final e in c.untracked) e['path'] as String,
|
||||
];
|
||||
unawaited(c.stage(paths));
|
||||
},
|
||||
if (c.untracked.isNotEmpty)
|
||||
_FileGroup(
|
||||
label: 'Untracked',
|
||||
entries: _applyFilter(c.untracked),
|
||||
actions: [
|
||||
_GroupAction(
|
||||
label: 'Stage all',
|
||||
onTap: () {
|
||||
final paths = [
|
||||
for (final e in c.untracked) e['path'] as String,
|
||||
];
|
||||
unawaited(c.stage(paths));
|
||||
},
|
||||
),
|
||||
],
|
||||
onStage: (path) => unawaited(c.stage([path])),
|
||||
),
|
||||
],
|
||||
onStage: (path) => unawaited(c.stage([path])),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -369,8 +368,7 @@ class _GitFileRow extends StatelessWidget {
|
||||
},
|
||||
builder: (context, hovered, _) => Container(
|
||||
color: hovered ? tokens.sidebarItemHover : null,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 20, right: 8, top: 2, bottom: 2),
|
||||
padding: const EdgeInsets.only(left: 20, right: 8, top: 2, bottom: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideText(
|
||||
|
||||
@@ -140,8 +140,7 @@ class _BranchPickerState extends State<_BranchPicker> {
|
||||
_loading = false;
|
||||
if (r.ok) {
|
||||
_branches = [
|
||||
for (final b in (r.data['branches'] as List? ?? const []))
|
||||
(b as Map).cast<String, Object?>(),
|
||||
for (final b in (r.data['branches'] as List? ?? const [])) (b as Map).cast<String, Object?>(),
|
||||
];
|
||||
} else {
|
||||
_error = r.error?.message ?? 'failed to load branches';
|
||||
@@ -197,10 +196,8 @@ class _BranchPickerState extends State<_BranchPicker> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (_error != null)
|
||||
Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
|
||||
if (_loading) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
|
||||
if (!_loading && _error == null && _branches.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('No branches found.', muted: true)),
|
||||
if (_branches.isNotEmpty)
|
||||
@@ -266,9 +263,7 @@ class _BranchRow extends StatelessWidget {
|
||||
name,
|
||||
fontFamily: clideMonoFamily,
|
||||
fontSize: clideFontMono,
|
||||
color: current
|
||||
? tokens.globalForeground
|
||||
: tokens.listItemForeground,
|
||||
color: current ? tokens.globalForeground : tokens.listItemForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -61,8 +61,7 @@ class _BacklinksViewState extends State<BacklinksView> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(
|
||||
c.activePath!.split('/').last,
|
||||
color: tokens.globalForeground,
|
||||
@@ -70,8 +69,7 @@ class _BacklinksViewState extends State<BacklinksView> {
|
||||
),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(
|
||||
c.error!,
|
||||
color: tokens.statusError,
|
||||
@@ -120,8 +118,7 @@ class _LinkGroup extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2),
|
||||
padding: const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2),
|
||||
child: ClideText(
|
||||
'$label (${links.length})',
|
||||
fontSize: clideFontCaption,
|
||||
@@ -133,8 +130,7 @@ class _LinkGroup extends StatelessWidget {
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2),
|
||||
child: ClideText('None', fontSize: clideFontCaption, muted: true),
|
||||
),
|
||||
for (final link in links)
|
||||
_LinkRow(link: link, pathKey: pathKey),
|
||||
for (final link in links) _LinkRow(link: link, pathKey: pathKey),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -159,21 +155,17 @@ class _LinkRow extends StatelessWidget {
|
||||
onTap: () {
|
||||
if (!target.startsWith('http')) {
|
||||
final kernel = ClideKernel.of(context);
|
||||
unawaited(
|
||||
kernel.ipc.request('editor.open', args: {'path': target}));
|
||||
unawaited(kernel.ipc.request('editor.open', args: {'path': target}));
|
||||
}
|
||||
},
|
||||
builder: (context, hovered, _) => Container(
|
||||
color: hovered ? tokens.sidebarItemHover : null,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
|
||||
child: ClideText(
|
||||
display,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: target.startsWith('http')
|
||||
? tokens.statusInfo
|
||||
: tokens.sidebarForeground,
|
||||
color: target.startsWith('http') ? tokens.statusInfo : tokens.sidebarForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -43,7 +43,10 @@ class _PqlPanelViewState extends State<PqlPanelView> {
|
||||
if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3);
|
||||
});
|
||||
});
|
||||
_fileSub = kernel.events.on<DaemonEvent>().where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && (e.data['path'] as String? ?? '').endsWith('.md')).listen((_) {
|
||||
_fileSub = kernel.events
|
||||
.on<DaemonEvent>()
|
||||
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && (e.data['path'] as String? ?? '').endsWith('.md'))
|
||||
.listen((_) {
|
||||
if (_controller?.view == PqlView.markdown) {
|
||||
unawaited(_controller!.loadMarkdownFiles());
|
||||
}
|
||||
@@ -83,8 +86,7 @@ class _PqlPanelViewState extends State<PqlPanelView> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3),
|
||||
),
|
||||
if (c.loading && c.results.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (c.loading && c.results.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (!c.loading && c.results.isEmpty && c.error == null && c.view == PqlView.markdown)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('No markdown files found.', muted: true)),
|
||||
Expanded(
|
||||
@@ -318,10 +320,7 @@ class _QueryResultRow extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final name = entry['name'] as String? ?? entry['path'] as String? ?? '';
|
||||
final values = entry.entries
|
||||
.where((e) => e.key != 'name' && e.key != 'path')
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.join(' · ');
|
||||
final values = entry.entries.where((e) => e.key != 'name' && e.key != 'path').map((e) => '${e.key}: ${e.value}').join(' · ');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
child: Column(
|
||||
|
||||
@@ -55,8 +55,7 @@ class ProblemsController extends ChangeNotifier {
|
||||
}
|
||||
final skill = (doctor.data['skill'] as Map?)?.cast<String, Object?>();
|
||||
if (skill != null) {
|
||||
final project =
|
||||
(skill['project'] as Map?)?.cast<String, Object?>();
|
||||
final project = (skill['project'] as Map?)?.cast<String, Object?>();
|
||||
if (project != null) {
|
||||
final state = project['state'] as String?;
|
||||
if (state == 'stale') {
|
||||
|
||||
@@ -49,7 +49,8 @@ class _ProblemsViewState extends State<ProblemsView> {
|
||||
explicitChildNodes: true,
|
||||
child: () {
|
||||
final lf = _filter.toLowerCase();
|
||||
final filtered = lf.isEmpty ? c.problems : c.problems.where((p) => p.message.toLowerCase().contains(lf) || p.source.toLowerCase().contains(lf)).toList();
|
||||
final filtered =
|
||||
lf.isEmpty ? c.problems : c.problems.where((p) => p.message.toLowerCase().contains(lf) || p.source.toLowerCase().contains(lf)).toList();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -64,16 +65,15 @@ class _ProblemsViewState extends State<ProblemsView> {
|
||||
label: 'refresh problems',
|
||||
child: GestureDetector(
|
||||
onTap: () => unawaited(c.refresh()),
|
||||
child: MouseRegion(cursor: SystemMouseCursors.click, child: ClideText('Refresh', fontSize: clideFontCaption, color: tokens.sidebarForeground)),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click, child: ClideText('Refresh', fontSize: clideFontCaption, color: tokens.sidebarForeground)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (c.loading && c.problems.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('Scanning…', muted: true)),
|
||||
if (!c.loading && filtered.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('No problems found.', muted: true)),
|
||||
if (c.loading && c.problems.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Scanning…', muted: true)),
|
||||
if (!c.loading && filtered.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('No problems found.', muted: true)),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
|
||||
@@ -139,9 +139,7 @@ class _TerminalPaneState extends State<TerminalPane> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = _error != null
|
||||
? _error!
|
||||
: (_paneId == null ? 'spawning shell…' : 'pid $_pid · ${_paneId!}');
|
||||
final subtitle = _error != null ? _error! : (_paneId == null ? 'spawning shell…' : 'pid $_pid · ${_paneId!}');
|
||||
|
||||
return ClidePaneChrome(
|
||||
title: 'terminal',
|
||||
|
||||
@@ -31,8 +31,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: i.string('modal.title',
|
||||
namespace: ThemePickerView.ns, placeholder: 'Select theme'),
|
||||
label: i.string('modal.title', namespace: ThemePickerView.ns, placeholder: 'Select theme'),
|
||||
explicitChildNodes: true,
|
||||
child: ClideSurface(
|
||||
width: 420,
|
||||
@@ -45,8 +44,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ClideText(
|
||||
i.string('modal.title',
|
||||
namespace: ThemePickerView.ns, placeholder: 'Select theme'),
|
||||
i.string('modal.title', namespace: ThemePickerView.ns, placeholder: 'Select theme'),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -65,9 +63,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
|
||||
displayName: t.displayName,
|
||||
selected: t.name == currentName,
|
||||
hovered: _hovered == t.name,
|
||||
hint: i.string('row.select.hint',
|
||||
namespace: ThemePickerView.ns,
|
||||
placeholder: 'Activate this theme'),
|
||||
hint: i.string('row.select.hint', namespace: ThemePickerView.ns, placeholder: 'Activate this theme'),
|
||||
onEnter: () => setState(() => _hovered = t.name),
|
||||
onExit: () => setState(() => _hovered = null),
|
||||
onTap: () {
|
||||
@@ -84,12 +80,9 @@ class _ThemePickerViewState extends State<ThemePickerView> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ClideButton(
|
||||
label: i.string('modal.cancel',
|
||||
namespace: ThemePickerView.ns, placeholder: 'Cancel'),
|
||||
semanticHint: i.string('modal.cancel.hint',
|
||||
namespace: ThemePickerView.ns,
|
||||
placeholder:
|
||||
'Close the theme picker without changing the current theme'),
|
||||
label: i.string('modal.cancel', namespace: ThemePickerView.ns, placeholder: 'Cancel'),
|
||||
semanticHint:
|
||||
i.string('modal.cancel.hint', namespace: ThemePickerView.ns, placeholder: 'Close the theme picker without changing the current theme'),
|
||||
onPressed: () => widget.onDismiss(),
|
||||
),
|
||||
],
|
||||
@@ -125,14 +118,8 @@ class _ThemeRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final bg = selected
|
||||
? tokens.listItemSelectedBackground
|
||||
: (hovered
|
||||
? tokens.listItemHoverBackground
|
||||
: tokens.listItemBackground);
|
||||
final fg = selected
|
||||
? tokens.listItemSelectedForeground
|
||||
: tokens.listItemForeground;
|
||||
final bg = selected ? tokens.listItemSelectedBackground : (hovered ? tokens.listItemHoverBackground : tokens.listItemBackground);
|
||||
final fg = selected ? tokens.listItemSelectedForeground : tokens.listItemForeground;
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
|
||||
@@ -40,6 +40,5 @@ class TicketTypeColors {
|
||||
bug: Color(0xFFC03030),
|
||||
);
|
||||
|
||||
static TicketTypeColors forTheme({required bool dark}) =>
|
||||
dark ? TicketTypeColors.dark : TicketTypeColors.light;
|
||||
static TicketTypeColors forTheme({required bool dark}) => dark ? TicketTypeColors.dark : TicketTypeColors.light;
|
||||
}
|
||||
|
||||
@@ -122,8 +122,7 @@ class _TicketHeader extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily),
|
||||
const Spacer(),
|
||||
if (detail.priority != null)
|
||||
ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
if (detail.priority != null) ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -153,13 +152,18 @@ class _StatusControls extends StatelessWidget {
|
||||
for (final s in _statuses) ...[
|
||||
Expanded(
|
||||
child: ClideTappable(
|
||||
onTap: detail.status == s ? null : () async {
|
||||
final resp = await controller.ipc.request('pql.tickets.status', args: {'ids': [detail.id], 'status': s});
|
||||
if (resp.ok) {
|
||||
controller.messages.publish('builtin.tickets', 'changed', {'id': detail.id});
|
||||
await controller.load(detail.id);
|
||||
}
|
||||
},
|
||||
onTap: detail.status == s
|
||||
? null
|
||||
: () async {
|
||||
final resp = await controller.ipc.request('pql.tickets.status', args: {
|
||||
'ids': [detail.id],
|
||||
'status': s
|
||||
});
|
||||
if (resp.ok) {
|
||||
controller.messages.publish('builtin.tickets', 'changed', {'id': detail.id});
|
||||
await controller.load(detail.id);
|
||||
}
|
||||
},
|
||||
builder: (ctx, hovered, _) {
|
||||
final active = detail.status == s;
|
||||
final color = active ? tokens.statusInfo : (hovered ? tokens.globalForeground : tokens.globalTextMuted);
|
||||
|
||||
@@ -88,7 +88,10 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (!mounted) return;
|
||||
if (_refreshing) { _pendingRefresh = true; return; }
|
||||
if (_refreshing) {
|
||||
_pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
_refreshing = true;
|
||||
_pendingRefresh = false;
|
||||
await _load();
|
||||
@@ -130,7 +133,11 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
final hasFilter = lf.isNotEmpty;
|
||||
final filtered = hasFilter ? _tickets.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf)).toList() : _tickets;
|
||||
final filtered = hasFilter
|
||||
? _tickets
|
||||
.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf))
|
||||
.toList()
|
||||
: _tickets;
|
||||
|
||||
const sections = [
|
||||
('in_progress', 'IN PROGRESS'),
|
||||
@@ -159,7 +166,8 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
child: ClideTappable(
|
||||
onTap: _refreshing ? null : _refresh,
|
||||
tooltip: 'Refresh tickets',
|
||||
builder: (ctx, hovered, _) => ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
builder: (ctx, hovered, _) =>
|
||||
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -115,9 +115,9 @@ class _StartColumn extends StatelessWidget {
|
||||
kernel.panels.activateTab(Slots.workspace, 'claude.primary');
|
||||
} else {
|
||||
kernel.dialog.show((ctx, dismiss) => _NotARepoDialog(
|
||||
path: picked,
|
||||
onDismiss: () => dismiss(),
|
||||
));
|
||||
path: picked,
|
||||
onDismiss: () => dismiss(),
|
||||
));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -163,8 +163,7 @@ class _ActionRow extends StatelessWidget {
|
||||
ClideIcon(icon, size: 18, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)),
|
||||
if (shortcut != null)
|
||||
ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
if (shortcut != null) ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -191,8 +190,7 @@ class _RecentColumn extends StatelessWidget {
|
||||
if (recents.isEmpty)
|
||||
const ClideText('No recent projects.', muted: true, fontSize: 14)
|
||||
else
|
||||
for (final r in recents)
|
||||
_RecentRow(project: r, tokens: tokens, onTap: () => _openRecent(r.path)),
|
||||
for (final r in recents) _RecentRow(project: r, tokens: tokens, onTap: () => _openRecent(r.path)),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -232,7 +230,9 @@ class _RecentRow extends StatelessWidget {
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(child: ClideText(project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
Flexible(
|
||||
child: ClideText(project.relativePath,
|
||||
muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
if (project.branch != null) ...[
|
||||
ClideText(' · ', muted: true, fontSize: 13),
|
||||
ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted),
|
||||
@@ -336,7 +336,10 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> {
|
||||
Future<void> _submit() async {
|
||||
final path = _controller.text.trim();
|
||||
if (path.isEmpty) return;
|
||||
setState(() { _loading = true; _error = null; });
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await widget.onOpen(path);
|
||||
} catch (_) {
|
||||
|
||||
Reference in New Issue
Block a user