Skip to content

feat: add Ink conversation picker and shared conversation defaults - #285

Open
Liangwei Wang (LiangweiOIO) wants to merge 2 commits into
mainfrom
feat/ink-agent-picker
Open

Liangwei Wang (LiangweiOIO) wants to merge 2 commits into
mainfrom
feat/ink-agent-picker

Conversation

@LiangweiOIO

@LiangweiOIO Liangwei Wang (LiangweiOIO) commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add an explicit Ink Inquiry destination picker for creating a conversation or continuing an existing Agent Node/thread. Lassoed Agent Nodes neither determine the destination nor become reference sources.
  • Default to the Space's most recently accepted anchored conversation, shared between desktop and iPad. Ink Inquiry and anchored ChatPanel submissions update the same server-owned pointer; continuation reuses the exact thread and its normal history without another context or memory layer.
  • When no recent target exists, continue a single available conversation, require a choice for ambiguous/unavailable candidates, and default to New only when there are no conversations. Busy, deleted, and invalid remembered targets never silently fall back.
  • Preserve manual destinations and rejected-send reservations when a replacement Lasso is cancelled. Refresh defaults only for a newly committed selection, including identical replacement selections.
  • Bind conversation storage to stable Workspace identity, resolve current history paths after Space renames, and hold Workspace-operation leases across acceptance and pending-record recovery.
  • Improve toolbar spacing, narrow-screen layout, and menu placement; hide the empty continuation group. Increase the optional OCR deadline from two to three seconds.

Scope

  • Includes production unit/browser regression coverage and updated architecture/proposal documentation.
  • Excludes the standalone Playground design page, its styles/tests, and local-only Tailscale/iPad development configuration.
  • Resource-based conversation recommendations remain deferred. Existing Spaces establish recency on their next qualifying acceptance; this change does not infer or migrate historical recency.

Review fixes

Addressed all three findings from the pre-PR code review:

  1. Continued submissions through live conversation handles after a Space rename.
  2. Workspace activation during queued acceptance or pending-record recovery.
  3. Cancelled replacement Lasso losing the previous manual destination.

Validation

  • 238 server tests across 10 relevant suites, including live-handle rename and Workspace-isolation regressions.
  • 88 frontend tests across six relevant suites.
  • Four Playwright browser tests covering cancellation, fresh-selection defaults, spacing, and responsive layout.
  • Repository pnpm typecheck, pnpm format, pnpm check:i18n, and git diff --check.
  • Pre-commit lint/format hooks.
  • Repository pnpm lint:fix --ignore-pattern tmp/dev-ipad.mjs: zero errors; existing warnings remain. The unmodified command encounters five no-undef errors only in that git-ignored local helper, which is not part of this PR.

PostgreSQL integration was not exercised. The persistence coordination retains the existing single-process lease/mutex guarantees rather than introducing cross-process transactions.

Reuse existing Agent sessions by default, persist per-Space accepted conversation recency, and keep lassoed Agent nodes out of Ink source targeting. Preserve cancelled Lasso drafts and guard conversation storage across Space renames and Workspace activation. Extend the OCR deadline to three seconds and add production regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: LiangweiOIO <64085199+LiangweiOIO@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The endpoint has a Workspace race, recovery does not converge, and picker focus behavior needs correction.

4 open findings
What changed in this PR

Adds an Ink conversation destination picker and server-owned recent-conversation defaults shared across clients.

Changes:

  • Adds explicit New/Continue destination selection with resilient Lasso state.
  • Persists accepted conversation recency with Workspace-aware storage.
  • Extends tests, documentation, responsive UI, and OCR timeout.
File Description
packages/​shared/​src/​types/​api/​canvas.ts Defines recent-conversation API schemas.
docs/​proposals/​ink-lasso-question-flow.md Documents picker and continuity design.
docs/​architecture/​web-architecture.md Records picker UI conventions.
docs/​architecture/​sketch-node.md Documents Ink destination behavior.
docs/​architecture/​canvas-storage.md Describes recency persistence.
docs/​architecture/​canvas-input-interactions.md Documents selection-independent targeting.
docs/​architecture/​api-design.md Specifies the new endpoint.
docs/​architecture/​agent-context.md Documents context exclusion and OCR.
apps/​web/​src/​store/​gesturePreviewStore.ts Adds committed-Lasso identity.
apps/​web/​src/​i18n/​resources/​zh-CN/​common.json Adds Chinese picker strings.
apps/​web/​src/​i18n/​resources/​en/​common.json Adds English picker strings.
apps/​web/​src/​hooks/​useCanvasLasso.gesture.test.tsx Tests Lasso identity lifecycle.
apps/​web/​src/​hooks/​areaSelectionSession.ts Commits and restores Lasso identity.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​useInkConversationDestination.ts Resolves destination defaults.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​StrokeSelectionToolbar.tsx Integrates picker and submission routing.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​StrokeSelectionToolbar.test.tsx Tests destination and retry behavior.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​NodeToolbar.css Adds responsive picker layout.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​inkQuestionSubmission.ts Separates sources from destinations.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​inkQuestionSubmission.test.ts Tests explicit target derivation.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​InkAgentDestinationPicker.tsx Implements the destination menu.
apps/​web/​src/​components/​Panels/​Canvas/​FloatingToolbars/​InkAgentDestinationPicker.test.tsx Tests menu interaction and localization.
apps/​web/​src/​api/​canvas.ts Adds recent-conversation fetch helper.
apps/​web/​src/​api/​_routes.ts Adds endpoint route generation.
apps/​web/​e2e/​ink-toolbar.spec.ts Covers picker browser workflows.
apps/​web/​e2e/​fixtures/​ink-toolbar.tsx Provides the browser-test fixture.
apps/​server/​src/​modules/​workspace/​paths.ts Rebinds conversation namespaces safely.
apps/​server/​src/​modules/​canvas/​canvas.route.ts Exposes the recency endpoint.
apps/​server/​src/​modules/​agent/​substrate-store.ts Adds strict document reads.
apps/​server/​src/​modules/​agent/​recent-conversation.ts Implements recency persistence and recovery.
apps/​server/​src/​modules/​agent/​recent-conversation.test.ts Tests storage and Workspace isolation.
apps/​server/​src/​modules/​agent/​conversation/​ink-ocr.ts Raises OCR deadline to three seconds.
apps/​server/​src/​modules/​agent/​conversation/​ink-ocr.test.ts Updates deadline coverage.
apps/​server/​src/​modules/​agent/​agenetes/​conversation-stores.ts Adds leases, rebinding, and recency hooks.
apps/​server/​src/​modules/​agent/​agenetes/​conversation-stores.test.ts Updates namespace test setup.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +598 to +602
if (!(await space(canvasId).read())) {
return reply.code(404).send({ message: 'Canvas not found' });
}
return reply.send(
await readRecentCanvasConversation(canvasId, conversationEventLogStore),
Comment on lines +129 to +135
const index = await readIndex(handle);
if (!index.pending) return index;
return {
conversation: (await findAcceptedStart(namespace, index.pending, events))
? index.pending.conversation
: index.conversation,
};
Comment on lines +149 to +152
onMouseDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
**Cross-region split / merge (drag).** A **pure** stroke selection (no whole node in the lasso) dropped onto **blank canvas** splits into a brand-new sketch region; dropped onto **another** sketch region it merges into it. On commit [useSketchStrokeMove.ts](../../apps/web/src/hooks/useSketchStrokeMove.ts) hit-tests the drop point in absolute flow (excluding the source regions, topmost wins) and, for a cross-region drop, dispatches the `MOVE_SKETCH_STROKES_TO_REGION` UI intent instead of the in-node translate. [resolveMoveSketchStrokesToRegion.ts](../../apps/web/src/handler/canvasCommand/resolvers/resolveMoveSketchStrokesToRegion.ts) resolves the destination parent frame from the drop point (`resolveFrameAtPoint`) and calls [buildSketchStrokeTransferCommands](../../apps/web/src/components/Nodes/sketch/sketchMerge.ts), which works in **absolute flow** (`getAbsolutePosition`) so transfers across frames stay correct and degrade to the plain in-node math when the parent is unchanged; the source side reuses the extracted pure core `computeEraseCommands` (reflow the remainder, or delete the node when emptied). Splitting **into a frame** shows the same grow-to-fit accept-preview a whole-node drag gets (`computeFrameFit` + `setFrameFitPreviews`); merge / in-place / blank-top-level drops show none. The whole transfer is **one undo entry** — `canvasStore.moveSketchStrokesToRegion` brackets it with `beginNodeDataGesture` / `endNodeDataGesture` (with an empty-op `rollbackGestureSnapshot` guard). Deferred: auto-contact ("bridging") merge, edge rewiring when a source is emptied, and OCR-rerun on split (see the [sketch-region-redesign proposal](../proposals/sketch-region-redesign.md)).

**Toolbar arbitration.** A [StrokeSelectionToolbar](../../apps/web/src/components/Panels/Canvas/FloatingToolbars/StrokeSelectionToolbar.tsx) floats above the stroke selection: on a **pure, single-color** selection it shows color + size controls (reusing [SketchControls](../../apps/web/src/components/Nodes/sketch/SketchControls.tsx), applied only to the selected strokes — the brush preset is untouched). Each color or size tick builds one `MERGE_NODE_DATA` command containing every affected sketch patch, matching the multi-node accent path's atomic update and undo semantics; the size slider's gesture bracket folds all ticks into one undo entry. A **Delete** action (touch only — desktop uses the keyboard) reuses the eraser's `buildEraseCommands` (subset removal → bbox reflow, or node delete when empty). Delete removes the **whole selection** — strokes plus any whole nodes the same lasso caught — as **one undo entry**: the node delete takes its snapshot + intent trace, then the stroke erase folds into that same entry via [commitStrokeCommands](../../apps/web/src/components/Nodes/sketch/sketchMerge.ts) (`foldIntoOpenGesture`), mirroring the mixed stroke-move gesture. **Delete / Backspace** triggers the same combined delete (guarded against text inputs); the canonical keyboard handler in [useCanvasShortcuts.ts](../../apps/web/src/hooks/shortcuts/useCanvasShortcuts.ts) **skips its own node deletion while a stroke selection is active** so the keypress never pushes a second snapshot. To guarantee at most one floating toolbar, the node toolbars (single-select in [NodeWrapper.tsx](../../apps/web/src/components/Nodes/NodeWrapper.tsx) and MultiSelect) hide whenever a stroke selection exists. Pure and mixed Ink selections retain the source count and submit action; an adjacent compact target hint shows `New · <recent Agent name>` when the Lasso contains no Question/Agent Node, the effective bound Agent name when it contains one valid target, or a blocked state for multiple or invalid targets. New Ink Questions refresh the Profile catalogue and apply the browser-local recent conversational Agent before creation, falling back to the first selectable external Profile when that cache is absent or stale and using Built-In operate or external ask mode; existing targets keep their binding. The target hint is metadata rather than a source and never changes the source count. Rendering a stroke selection to PNG and sending it to the agent is covered in §4.1. Optional submission-time OCR uses only the explicitly selected strokes as transient Agent evidence and does not persist recognized text into the Sketch; eager/background region OCR remains deferred (see [sketch-region-redesign proposal](../proposals/sketch-region-redesign.md) Stage 3).
**Toolbar arbitration.** A [StrokeSelectionToolbar](../../apps/web/src/components/Panels/Canvas/FloatingToolbars/StrokeSelectionToolbar.tsx) floats above the stroke selection: on a **pure, single-color** selection it shows color + size controls (reusing [SketchControls](../../apps/web/src/components/Nodes/sketch/SketchControls.tsx), applied only to the selected strokes — the brush preset is untouched). Each color or size tick builds one `MERGE_NODE_DATA` command containing every affected sketch patch, matching the multi-node accent path's atomic update and undo semantics; the size slider's gesture bracket folds all ticks into one undo entry. A **Delete** action (touch only — desktop uses the keyboard) reuses the eraser's `buildEraseCommands` (subset removal → bbox reflow, or node delete when empty). Delete removes the **whole selection** — strokes plus any whole nodes the same lasso caught — as **one undo entry**: the node delete takes its snapshot + intent trace, then the stroke erase folds into that same entry via [commitStrokeCommands](../../apps/web/src/components/Nodes/sketch/sketchMerge.ts) (`foldIntoOpenGesture`), mirroring the mixed stroke-move gesture. **Delete / Backspace** triggers the same combined delete (guarded against text inputs); the canonical keyboard handler in [useCanvasShortcuts.ts](../../apps/web/src/hooks/shortcuts/useCanvasShortcuts.ts) **skips its own node deletion while a stroke selection is active** so the keypress never pushes a second snapshot. To guarantee at most one floating toolbar, the node toolbars (single-select in [NodeWrapper.tsx](../../apps/web/src/components/Nodes/NodeWrapper.tsx) and MultiSelect) hide whenever a stroke selection exists. Pure and mixed Ink selections retain the source count, explicit conversation picker, and submit action. The picker is not a source; lassoed Agent Nodes never determine its destination or contribute source counts. Conversation-first defaults, explicit New, and existing-thread continuation are described in §4.1. Rendering a stroke selection to PNG and sending it to the agent is also covered there. Optional submission-time OCR uses only the explicitly selected strokes as transient Agent evidence and does not persist recognized text into the Sketch; eager/background region OCR remains deferred (see [sketch-region-redesign proposal](../proposals/sketch-region-redesign.md) Stage 3).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants