Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
853b0eb
feat(right-panel): comment mode for Code tab with clipboard flush
srid May 13, 2026
7389719
refactor(hickey): rename setCommentMode_ to disableCommentMode
srid May 13, 2026
3a7b5cb
refactor(hickey): split useCommentMode out of useComments
srid May 13, 2026
7728360
fix(hickey): sweep empty buckets from useComments singleton cache
srid May 13, 2026
6174235
refactor(lowy): version the persisted Comment[] bucket shape
srid May 13, 2026
a4f119b
refactor(hickey): synchronous onChange in useLineSelection
srid May 13, 2026
87ac827
refactor(lowy): hoist CodeMenuFrame out of BrowseFileView
srid May 13, 2026
03bdaba
fix(hickey): disable close button when comments are queued
srid May 13, 2026
4f497b0
refactor(lowy): move composer draft back into CommentsTray
srid May 13, 2026
e867ca4
fix(police): fact-check β€” fire onChange synchronously on mount
srid May 13, 2026
a2589c6
fix(police): caught-error-must-not-collapse-to-empty in deserializeBu…
srid May 13, 2026
74d60fc
fix(police): e2e-poll-async-state for clipboard assertions
srid May 13, 2026
ec89815
fix(police): dry-rule-of-three β€” extract assertCommentCount helper
srid May 13, 2026
29ddc77
refactor(police): elegance β€” use writeTextToClipboard for non-secure-…
srid May 13, 2026
6651e00
refactor(police): elegance β€” share formatRange between lineRef and co…
srid May 13, 2026
5cc3dce
refactor(police): elegance β€” crypto.randomUUID() for comment ids
srid May 13, 2026
bc46b29
refactor(police): elegance β€” CommentIcon + align toggle with ModeChip…
srid May 13, 2026
15c4e64
refactor(police): elegance β€” extract commentsTestIds + tray button class
srid May 13, 2026
81ba19f
refactor(police): elegance β€” withBucket helper in useComments
srid May 13, 2026
ac0b09c
refactor(police): elegance β€” memoize comments() in CommentsTray
srid May 13, 2026
c0fe0c0
refactor(police): elegance β€” trim narrating-WHAT comments
srid May 13, 2026
ff65708
test(comments): reorder scenarios so file mounts before tray opens
srid May 13, 2026
baa6755
docs(agency): track website/src/pages/index.astro in the /do docs list
srid May 13, 2026
a516bea
docs(website): add comment-mode as feature 09 on the landing page
srid May 13, 2026
177d65e
feat(comments): standardize clipboard payload as Markdown with path:L…
srid May 13, 2026
4fd7248
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
051e76d
feat(comments): register toggleCommentMode action β€” keybind, palette,…
srid May 13, 2026
5c86714
feat(comments): right-click "Add comment on path:Lrange" in Pierre menu
srid May 13, 2026
4b9c280
feat(comments): chrome-bar count badge for queued comments
srid May 13, 2026
8ccc375
docs(readme): mention the four discoverability surfaces for comment mode
srid May 13, 2026
1e8fb36
feat(comments): drop the [kolu comments v1] envelope from clipboard p…
srid May 13, 2026
93a0daf
feat(comments): inline composer popover at the selected line + edit/j…
srid May 13, 2026
c688fe7
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
382fcbc
fix(comments): anchor inline popover to the line via shadow-DOM-aware…
srid May 13, 2026
2fefd63
refactor(comments): drop the L prefix from clipboard line refs
srid May 13, 2026
9025917
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
63d659e
feat(comments): line-anchored bubbles for new (+) and existing (πŸ’¬) β€” …
srid May 13, 2026
d979026
fix(comments): + bubble flips to πŸ’¬ on submit, no orphan on tab switch
srid May 13, 2026
7474399
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
2f656de
fix(comments): πŸ’¬ bubble now finds the line in diff mode via [data-line]
srid May 13, 2026
32428ae
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
714953b
fix(comments): bubbles + popover work uniformly across browse/local/b…
srid May 13, 2026
aa598ad
Merge remote-tracking branch 'origin/master' into juicy-gum
srid May 13, 2026
f6842f9
refactor(comments): extract usePierreLineAnchor + collapse intent state
srid May 13, 2026
590e1cc
fix(comments): orphan-clear effects ignore value-stable signal ticks
srid May 14, 2026
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
1 change: 1 addition & 0 deletions .agency/do.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Keep these docs in sync:

- **`README.md`** (top-level) β€” user-facing changes, architecture prose, transport-resilience description.
- **`packages/surface/README.md`** β€” the `@kolu/surface` framework reference. The "How Kolu uses this framework" section is a concrete inventory of every cell, collection, and stream descriptor plus the raw-oRPC procedures that stay outside the framework. Update it whenever a new descriptor lands or whenever a contract entry's classification changes (added mutation, retired stream, …).
- **`website/src/pages/index.astro`** β€” the kolu.dev landing page. Its hand-maintained `features` array (and the `NN features` count chip) must mirror the top-level `README.md`'s feature sections: when you add a `### NewThing` section to the README's Features region, add a corresponding tile to `features` and bump the chip count. The website is deployed independently (its own flake under `website/`), but treating the README and the landing page as one set keeps the public face from drifting behind the project's own description.

## PR evidence

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@ Detects [OpenCode](https://github.com/anomalyco/opencode) sessions and shows the

- <kbd>Ctrl+V</kbd> pastes images into any agent that accepts paste-as-file-path (Claude Code, codex, …) β€” the server saves the browser's clipboard image and bracketed-pastes its path into the PTY

### Comments

Annotate any file the agent wrote (or any file at all β€” code, markdown, configs) directly from the Code tab and flush the queue to your clipboard. Paste the resulting block into whichever agent terminal makes sense; no per-agent wiring, no `kolu artifact` CLI, no server-side delivery.

- **Four ways to start** β€” πŸ’¬ chip in the Code-tab toolbar, <kbd>Cmd/Ctrl+Shift+/</kbd> keybind, command palette entry, or right-click any selected line and pick *Add comment on `path:Lrange`*. The chrome bar also surfaces a `πŸ’¬ N` count badge whenever the active worktree has queued comments, so you don't have to remember the queue is there
- **Inline composer at the line** β€” click a line in comment mode and a tiny composer pops up anchored next to that line (GitHub-PR style). <kbd>Enter</kbd> submits, <kbd>Shift+Enter</kbd> inserts a newline, <kbd>Esc</kbd> dismisses. No mouse trip to a bottom textbox; the popover comes to you
- **Tray = read-only roll-up** β€” bottom of the Code tab lists queued comments with click-line-ref-to-jump, ✎ to edit (re-opens the inline composer prefilled), and πŸ—‘ to drop one. The header bar paints itself with an accent fill whenever the queue is non-empty so you don't accidentally close the tab on unsynced notes
- **File-tree decoration** β€” files with queued comments get a `●` badge in the Code-tab file tree (via Pierre's `renderRowDecoration`), so the queue is discoverable from the top of the tree, not just from the bottom tray
- **Pinpoint targets** β€” Pierre's existing line-selection (click a line, drag for a range) drives the popover's target chip; works uniformly across browse mode and the local/branch diff modes, so `.md`, `.ts`, and any other text file all accept comments without per-kind logic
- **Cross-file accumulation** β€” comments collect across every file you visit; the tray sorts by (path, startLine) so a paste reads as a coherent walk through the repo, not click order
- **Plain Markdown clipboard payload** β€” flushed text is a Markdown bullet list with a code-spanned line ref per entry: `` - `path:start-end` β€” note ``. Agent-native format (matches `grep -n`, ripgrep, vim, VS Code, and what `claude` / `codex` / `opencode` parse for their `Read` tool) β€” no `L` prefix. No envelope or wrapper either: paste it raw, or prefix your own framing (`"Apply these comments:"`, `"## Review notes"`) before pasting
- **Copy-and-clear** β€” "Copy to clipboard" is destructive by design (issue [#878](https://github.com/juspay/kolu/issues/878)): users want the tray empty for the next review pass once feedback has been pasted into an agent prompt
- **Per-worktree persistence** β€” the in-progress queue is keyed by `repoRoot` in localStorage via `makePersisted`, so an accidental reload doesn't lose work and switching between worktree terminals shows each worktree's own tray

### Screen recording

Record the Kolu tab β€” whole canvas or a single maximized terminal β€” with microphone and optional webcam PiP, straight to a local `.webm` file. Chromium-only (uses the File System Access API).
Expand Down
10 changes: 10 additions & 0 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import MobileTileView from "./MobileTileView";
import { useRecorder } from "./recorder/useRecorder";
import WebcamOverlay from "./recorder/WebcamOverlay";
import RightPanelLayout from "./right-panel/RightPanelLayout";
import { toggleCommentMode } from "./right-panel/useCommentMode";
import { useRightPanel } from "./right-panel/useRightPanel";
import { client } from "./wire";
import { serverProcessId, wsStatus } from "./rpc/rpc";
Expand Down Expand Up @@ -214,6 +215,14 @@ const App: Component = () => {
handleScreenshotTerminal: () => handleScreenshotTerminal(),
toggleRightPanel: rightPanel.togglePanel,
toggleRecordingPause: () => useRecorder().togglePause(),
// Opening the right panel + Code tab + ensuring the tray appears even
// when comments is empty puts the user one click from the composer β€”
// the keybind/palette flow doesn't make sense if mode toggles on but
// the tray is hidden behind the inspector panel.
toggleCommentMode: () => {
rightPanel.openCodeBrowser();
toggleCommentMode();
},
};

useShortcuts(actionContext);
Expand Down Expand Up @@ -468,6 +477,7 @@ const App: Component = () => {
<ChromeBar
status={wsStatus()}
onOpenPalette={() => openPalette()}
activeRepoRoot={() => store.activeMeta()?.git?.repoRoot ?? null}
workspaceSwitcher={
<WorkspaceSwitcher
entries={workspaceEntries()}
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/ChromeBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useViewPosture } from "./canvas/useViewPosture";
import { ACTIONS } from "./input/actions";
import { formatKeybind } from "./input/keyboard";
import RecordButton from "./recorder/RecordButton";
import CommentCountBadge from "./right-panel/CommentCountBadge";
import { useRightPanel } from "./right-panel/useRightPanel";
import type { WsStatus } from "./rpc/rpc";
import SettingsPopover from "./settings/SettingsPopover";
Expand All @@ -44,6 +45,10 @@ const ChromeBar: Component<{
* ChromeBar is a layout host (logo + switcher + controls); it doesn't need
* to know the switcher's prop shape, just where to drop it. */
workspaceSwitcher: JSX.Element;
/** Active terminal's repo root, used to scope the comments-count badge
* to that worktree. Accessor so the badge re-reads when the user
* switches terminals across worktrees. */
activeRepoRoot: () => string | null;
}> = (props) => {
const rightPanel = useRightPanel();
const posture = useViewPosture();
Expand Down Expand Up @@ -122,6 +127,7 @@ const ChromeBar: Component<{
* area covered when the cluster overlaps the right panel pass
* clicks through; each button re-enables pointer-events-auto. */}
<div class="flex items-center gap-2 shrink-0">
<CommentCountBadge activeRepoRoot={props.activeRepoRoot} />
<RecordButton />
<Tip
label={`Toggle inspector (${formatKeybind(ACTIONS.toggleRightPanel.keybind)})`}
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ export function createCommands(deps: CommandDeps): Accessor<PaletteCommand[]> {
]
: []),
actionPaletteCommand("toggleRightPanel", deps),
actionPaletteCommand("toggleCommentMode", deps, {
description:
"Open the Code tab and toggle annotation mode for clipboard-flush feedback",
}),
...(!deps.isMobile()
? [
actionPaletteCommand("openWorkspaceSwitcher", deps),
Expand Down
9 changes: 9 additions & 0 deletions packages/client/src/input/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface ActionContext {
handleScreenshotTerminal: () => void;
toggleRightPanel: () => void;
toggleRecordingPause: () => void;
toggleCommentMode: () => void;
}

interface AppActionBase {
Expand Down Expand Up @@ -239,6 +240,14 @@ const _ACTIONS = {
keybind: { key: ".", code: "Period", mod: true, shift: true },
handler: (ctx) => ctx.toggleRecordingPause(),
},
toggleCommentMode: {
label: "Toggle comment mode",
// Mod+Shift+/ β€” slash is the universal "?" key, and Shift+? reads
// as "query / note?" which is the right mnemonic for "annotate."
// Avoids the Mod+/ chord that ShortcutsHelp owns.
keybind: { key: "?", code: "Slash", mod: true, shift: true },
handler: (ctx) => ctx.toggleCommentMode(),
},
} satisfies Record<string, AppAction>;

export type ActionId = keyof typeof _ACTIONS;
Expand Down
73 changes: 29 additions & 44 deletions packages/client/src/right-panel/BrowseFileView.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
/** File content viewer for the Code tab's browse mode. Subscribes to the
* server's live file-content stream so editor saves and branch checkouts
* reflect without a manual refresh. The wrapper around `@kolu/solid-pierre`'s
* `FileView` provides shiki-powered syntax highlighting; equality-gating
* the snapshot via `reconcile` (inside `useStream`'s underlying primitive)
* avoids stomping scroll position on no-op ticks. */
* reflect without a manual refresh. The `LineSelection` controller is
* owned one level up so this view and the diff view share a single
* `CodeMenuFrame` wrap. */

import {
FileView,
type SelectedLineRange,
Virtualizer,
} from "@kolu/solid-pierre";
import { FileView, Virtualizer } from "@kolu/solid-pierre";
import { type Component, Match, Show, Switch } from "solid-js";
import { toast } from "solid-sonner";
import { pierreDiffsStyle } from "../ui/pierreTheme";
import type { LineSelection } from "../ui/useLineSelection";
import { app } from "../wire";
import CodeMenuFrame from "./CodeMenuFrame";

export type BrowseFileViewProps = {
repoPath: string;
filePath: string;
theme: "light" | "dark";
/** Initial line range to highlight (and scroll to). Set when the
* caller opens the file at a specific range β€” e.g. a terminal
* `path:line` click. Goes through the line-selection controller
* so the right-click "Copy path:N" menu reflects the highlight. */
initialSelectedLines?: SelectedLineRange | null;
/** Line-selection controller owned by the parent `CodeMenuFrame`. */
selection: LineSelection;
};

const BrowseFileView: Component<BrowseFileViewProps> = (props) => {
Expand All @@ -46,39 +38,32 @@ const BrowseFileView: Component<BrowseFileViewProps> = (props) => {
{(fc) => (
<>
<Show when={fc().truncated}>
<div class="px-2 py-1 text-warning text-[10px] border-b border-edge bg-surface-1/30">
<div class="px-2 py-1 text-warning text-[10px] border-b border-edge bg-surface-1/30 shrink-0">
File truncated (exceeds 1 MB)
</div>
</Show>
<CodeMenuFrame
path={props.filePath}
initialSelectedLines={props.initialSelectedLines}
{/* `<Virtualizer>` upgrades `<FileView>` to Pierre's
* `VirtualizedFile` for very large files
* (#809 / #514 Phase 8). Without it, `<FileView>` uses
* the vanilla `File` class β€” same behavior as before. */}
<Virtualizer
class="flex-1 min-h-0 overflow-auto"
style={pierreDiffsStyle}
>
{(selection) => (
// `<Virtualizer>` upgrades `<FileView>` to Pierre's
// `VirtualizedFile` for very large files
// (#809 / #514 Phase 8). Without it, `<FileView>` uses
// the vanilla `File` class β€” same behavior as before.
<Virtualizer
class="h-full w-full overflow-auto"
style={pierreDiffsStyle}
>
<FileView
name={props.filePath}
contents={fc().content}
theme={props.theme}
overflow="wrap"
enableLineSelection
onLineSelected={selection.handleSelect}
selectedLines={selection.range()}
onError={(err) =>
toast.error(`File render failed: ${err.message}`)
}
class="w-full"
/>
</Virtualizer>
)}
</CodeMenuFrame>
<FileView
name={props.filePath}
contents={fc().content}
theme={props.theme}
overflow="wrap"
enableLineSelection
onLineSelected={props.selection.handleSelect}
selectedLines={props.selection.range()}
onError={(err) =>
toast.error(`File render failed: ${err.message}`)
}
class="w-full"
/>
</Virtualizer>
</>
)}
</Match>
Expand Down
12 changes: 11 additions & 1 deletion packages/client/src/right-panel/CodeMenuFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Component, JSX } from "solid-js";
import {
CodeContextMenu,
type CodeContextMenuController,
type CodeContextMenuItem,
} from "../ui/CodeContextMenu";
import { type LineSelection, useLineSelection } from "../ui/useLineSelection";

Expand All @@ -25,20 +26,29 @@ export type CodeMenuFrameProps = {
* controller so a terminal `path:line` click drives both the
* Pierre highlight AND the right-click menu's "Copy path:N" item. */
initialSelectedLines?: SelectedLineRange | null;
/** Forward selection changes to a parent (e.g. CodeTab's comments
* tray composer). Fires on every commit, including null. */
onSelectionChange?: (range: SelectedLineRange | null) => void;
/** Extra context-menu items contributed by the caller β€” appended
* after the built-in "Copy path" / "Copy path:N" entries. Receives
* the current selection range (null when nothing is selected). */
extraMenuItems?: (range: SelectedLineRange | null) => CodeContextMenuItem[];
};

export const CodeMenuFrame: Component<CodeMenuFrameProps> = (props) => {
let menuCtrl: CodeContextMenuController | undefined;
const selection = useLineSelection(() => props.path, {
initialRange: () => props.initialSelectedLines,
onChange: (range) => props.onSelectionChange?.(range),
extraItems: (range) => props.extraMenuItems?.(range) ?? [],
});
return (
<div
// Attach contextmenu via addEventListener so the host div doesn't
// carry interactive JSX props β€” the inner Pierre canvas is the
// actual interactive surface; the host is layout only.
ref={(el) => el.addEventListener("contextmenu", (e) => menuCtrl?.open(e))}
class="h-full w-full"
class="flex flex-col h-full w-full"
>
{props.children(selection)}
<CodeContextMenu
Expand Down
Loading