You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by "ARE404 (via GitHub)" <gi...@apache.org> on 2026/09/11 14:18:21 UTC

[PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

ARE404 opened a new pull request, #5198:
URL: https://github.com/apache/maka/pull/5198

   ## Summary
   
   WorkHub conversations now identify delegated work with separate message accents (prompt on the right, answer on the left), compact colored Workspace / Session labels, and status text before the prompt timestamp. The standalone delegation result card is removed.
   
   Ambiguous routing can pause before Turn admission and show a target selector in the composer area. Nothing is preselected: number keys and arrows select, Enter confirms, and Esc returns to the preserved draft. The Host validates the offered opaque candidate identity, refreshes the same target against current candidates, and leaves execution authorization with the existing Action Gate. Expired or unavailable targets require another selection.
   
   WorkHub also exposes `AskUserQuestion` through its tool profile and renders the shared question panel, including pending-question hydration, answer retry, and Stop. Prompt statuses cover coordination before delegation as well as delegated work. Ordinary question panels share keyboard selection and display submission errors without discarding answers.
   
   ## Verification
   
   - Desktop build, typecheck, lint, format, renderer architecture, ASF headers, and Astryx surface inventory checks passed.
   - WorkHub coordinator, protocol, and Desktop send regression tests: 51 passed; the final send regression rerun passed all 15 tests.
   - Root Turn, Action Gate, hosted tool profile, and Desktop bridge regression run during target-selector implementation: 136 passed.
   - Latest hosted tool profile and execution-model composition run: 39/40 passed initially; the child-patch test timed out, then passed in isolation (1/1).
   - Native Electron Storybook interaction checks passed for target keyboard selection, selection submission failure, question hydration and retry to completion, question Stop, and ordinary question keyboard choices. These use scripted service fixtures and include accessibility-tree checks.
   - Native WorkHub return/navigation regression: 2 passed during selector implementation.
   - Dark narrow-window selector rendering inspected.
   
   Conversation identity and timestamp status (captured during this branch's status implementation):
   
   ![Conversation identity and status](https://raw.githubusercontent.com/ARE404/maka-agent/feat/workhub-conversation-colors/docs/images/pr/workhub-conversation-colors/conversation-status.png)
   
   Target selection, waiting status, and keyboard hints (latest implementation, scripted native Electron fixture):
   
   ![Target selection](https://raw.githubusercontent.com/ARE404/maka-agent/feat/workhub-conversation-colors/docs/images/pr/workhub-conversation-colors/target-selection.png)
   
   ## Remaining acceptance
   
   - [ ] Run the complete ambiguous-routing → user selection → target execution flow against a real model service.
   - [ ] Exercise the actual floating WorkHub window: automatic expansion from progress mode, keyboard focus, and floating/docked transitions with a pending question. Both placements share the implementation, but narrow-window fixture checks do not establish these behaviors.
   
   ## Protocol compatibility
   
   Runtime Host compatibility epoch increases from 142 to 143 because an answer response can now request target selection without admitting a Turn. Pending selector offers are bounded transient Host state; admitted routing decisions use the existing durable execution record.
   
   ## AI use
   
   - [ ] No generative tool made a substantive contribution
   - [x] Generative tooling made a substantive contribution
   
   Tool(s) and scope: Codex implemented the changes, tests, and PR description. Affected commits include `Generated-by: Codex`; retain this trailer when squashing.
   
   ## Checklist
   
   - [x] Tests cover the change and fail without it
   - [x] Lint, format, typecheck and the affected suites pass locally
   
   Does this PR entail a change in behavior?
   
   - [x] Yes — described under Summary above
   - [ ] No
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3996038016


##########
apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx:
##########
@@ -90,6 +102,12 @@ export function WorkHubRoot() {
   const progress = presentation?.progressRequest !== undefined;
   const editingProgress = progress && editingProgressRequest === presentation.progressRequest;
   const floating = presentation?.placement === 'floating';
+  useEffect(() => {
+    if (controller.targetSelection || controller.activeQuestion) {
+      setConversationExpanded(true);
+      void services.presentation.showConversation(presentation?.progressRequest).catch(controller.report);

Review Comment:
   **[P2] Preserve docked placement when a question or target selector appears**
   
   On the normal docked path, `presentation.progressRequest` is undefined. This effect calls `showConversation(undefined)` whenever either interaction appears; preload forwards the command unchanged, and `workhub-presentation.ts:537–561` takes its non-progress branch, calling `detach(true)`. That moves the live view into a focused floating window even though the user did not request a placement change. Pending-question hydration reaches the same path.
   
   Could this keep the local expansion update but send the main-process reveal command only for a presentation state that actually requires it, such as an existing progress card? Please cover a docked question/selector with an assertion that placement remains docked. The current Storybook `showConversation` stub is a no-op, so it misses the real command's detach behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3995423763


##########
apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx:
##########
@@ -279,6 +304,7 @@ export function WorkHubRoot() {
               allowAttachmentImportWhileStreaming
               stopPending={controller.stopPending}
               onSend={async (text, attachments, followUpMode) => {
+                selectWork(undefined);

Review Comment:
   **[P2] Clear the Work filter when retrying a failed send too**
   
   The ordinary composer `onSend` now calls `selectWork(undefined)`, but the error bar’s Retry button (line 279) goes directly to `controller.retry`. Its rejected-attempt branch calls `send(...)` without clearing the filter, so the new Turn is still excluded by `WorkHubConversation`'s `matchingTurns` filter before it acquires a Work link.
   
   This is a reachable failure/retry path: let a send fail, filter to an existing Work using its message stripe, then click Retry. An exact-head DOM probe using the real WorkHubRoot/controller and the PR's service fixtures confirmed that the second answer succeeded while the retry prompt and answer remained hidden; clearing the filter immediately revealed both.
   
   Could Retry clear the presentation filter before resubmitting, or share the same pre-send entry point as the composer? A failed-send → filter → Retry regression would cover the entry point the current ordinary-send test misses.



##########
apps/desktop/src/renderer/styles/workhub.css:
##########
@@ -92,67 +92,25 @@
   padding: 12px 0;
 }
 
-/* Identity sits next to the prompt; shared chat bubbles remain neutral. */
-.workhub-message-identity {
-  display: flex;
-  justify-content: flex-end;
-  min-width: 0;
-}
-
-.workhub-result-card {
-  display: grid;
-  gap: 6px;
-  width: min(100%, 34rem);
-  padding: 9px 11px;
-  border: 1px solid color-mix(in oklch, var(--workhub-work-color) 28%, transparent);
-  border-radius: var(--radius-md);
-  background: color-mix(in oklch, var(--workhub-work-color) 5%, transparent);
-}
-
-.workhub-result-heading {
-  display: flex;
-  align-items: baseline;
-  justify-content: space-between;
-  gap: 12px;
-  color: var(--workhub-work-label);
-  font-size: var(--font-size-body-sm, 12px);
-}
-
-.workhub-result-heading span,
-.workhub-result-card p {
-  margin: 0;
+/* Delegation status shares the prompt's timestamp row. */
+.workhub-delegation-status {
   color: var(--muted-foreground);
-  font-size: var(--font-size-body-xs, 12px);
 }
 
-.workhub-result-card p {
-  overflow: hidden;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 4;
-  white-space: normal;
-}
-
-.workhub-message-identity button {
-  height: auto;
-  min-height: 28px;
-  max-width: 100%;
-  white-space: normal;
-  overflow-wrap: anywhere;
-  text-align: right;
-  color: var(--workhub-work-label);
-  font-size: var(--font-size-body-sm, 12px);
-}
+.workhub-surface { --workhub-identity-label-tone: 0.42 0.075; }
+.dark .workhub-surface { --workhub-identity-label-tone: 0.8 0.09; }
 
 .workhub-work-identity {
   --workhub-work-color: oklch(0.58 0.1 var(--workhub-work-hue));
-  --workhub-work-label: oklch(0.42 0.075 var(--workhub-work-hue));
+  --workhub-work-label: oklch(var(--workhub-identity-label-tone) var(--workhub-work-hue));
   --workhub-work-highlight: oklch(0.48 0.18 var(--workhub-work-hue));
 }
 
-.dark .workhub-work-identity {
+.dark .workhub-surface { --workhub-identity-label-tone: 0.42 0.075; }
+.dark .workhub-surface { --workhub-identity-label-tone: 0.8 0.09; }
+
+.workhub-work-identity {
   --workhub-work-color: oklch(0.76 0.105 var(--workhub-work-hue));
-  --workhub-work-label: oklch(0.8 0.09 var(--workhub-work-hue));
   --workhub-work-highlight: oklch(0.85 0.15 var(--workhub-work-hue));

Review Comment:
   **[P2] Restore the dark-theme scope on these identity colors**
   
   This second `.workhub-work-identity` rule is now global, so it overrides the light-theme highlight values declared above. On the normal hover/keyboard-focus path, `[data-work-highlighted='true']` uses this dark-theme highlight for label text on a light background.
   
   An Electron computed-style probe at hue 250 produced `rgb(43, 80, 116)` for normal light-theme text (8.39:1 against white), but `rgb(124, 212, 255)` after highlighting (1.65:1). The intended light highlight produces 6.36:1 against white.
   
   Could this selector be restored to `.dark .workhub-work-identity`, and the duplicate `.dark .workhub-surface` declarations immediately above it be removed? That preserves the shared label tone while keeping the two highlight palettes scoped correctly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): unify work identity and Host-owned target selection [maka]

Posted by "ARE404 (via GitHub)" <gi...@apache.org>.
ARE404 merged PR #5198:
URL: https://github.com/apache/maka/pull/5198


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on PR #5198:
URL: https://github.com/apache/maka/pull/5198#issuecomment-5645554318

   Thanks for working through the review feedback. I think this now needs a bounded architecture consolidation before another rereview, rather than only a guard around the latest P2. This assessment is based on `1de55db5d7601d8875c13a563d8bc346e2add8d4`.
   
   The Host admission and execution boundary looks sound. The remaining pattern is in UI orchestration: submission visibility is enforced separately by Send and Retry, while `showConversation` mixes expanding content with changing window placement. Fixing individual callers leaves the next caller exposed to the same assumptions. The Storybook presentation stub also hides the actual window-command behavior. I should have distinguished these UI contracts from the already-correct Host architecture more clearly in the earlier review.
   
   Please give your implementation agent the following prompt. The goal is to remove those recurring coordination obligations within this PR, not to add another controller or redesign all of WorkHub.
   
   ---
   
   ### Implementation agent prompt
   
   Review and consolidate the UI orchestration in this PR before fixing the latest inline P2. Start by reading the current production paths and the review history; do not assume every previous suggestion is correct. Pin your work to the latest PR head and account for changes from main.
   
   **1. Define the contracts and their owners before editing.**
   
   Trace ordinary Send, rejected-send Retry, unknown-admission recovery, transcript-read recovery, target selection, AskUserQuestion hydration, and progress/floating/docked presentation through the renderer, preload, and main process. State which existing owner decides each fact and which side effects each entry is allowed to cause. In particular:
   
   - Runtime Host remains the sole authority for admission, execution, interactions, and terminal state. Renderer state is presentation or pending-request coordination.
   - A newly submitted or resubmitted prompt and its answer must be visible without manually clearing a Work filter. Read recovery and unknown-admission reconciliation are not new submissions and must not accidentally resend work.
   - Presenting a question or target selector must preserve the current docked/floating placement. Expanding an existing progress card is a distinct operation from explicitly detaching or docking the window.
   - Pending interactions must retain their answers/selection and usable keyboard focus through supported presentation transitions; focusing an ordinary composer must not override an active interaction.
   
   **2. Consolidate at the existing boundaries.**
   
   - Remove the need for individual Send/Retry buttons to remember the same submission-visibility rule. Put that rule at an existing shared submission boundary, while preserving the distinct retry/recovery semantics above. Do not move presentation filtering into Runtime Host.
   - Inspect every production caller of `showConversation`. Today an absent `progressRequest` can reach `detach(true)`, although the caller only intends to reveal an interaction. Narrow this contract so expanding a progress card cannot silently mean changing placement. Check whether requiring the current progress request lets the optional/no-request branch disappear; keep explicit placement changes on the existing detach/dock path. Validate stale requests at the existing presentation owner.
   - Keep local content expansion, native placement, and focus responsibilities explicit. Do not introduce a second presentation state machine, mirrored lifecycle flags, or another event loop to synchronize them. If additional state is truly necessary, identify the concrete obligation that cannot be met by an existing owner.
   - Preserve the multi-Turn buffer and stable rejected-prompt identity from the latest main integration.
   
   **3. Perform an ablation pass.**
   
   After the consolidation, try removing the superseded branches, duplicated caller-side side effects, and tests that only assert those old mechanisms. Retain the smallest tests protecting distinct observable contracts. Do not delete the Host offer cache, fresh candidate rebinding, durable routing decision, or Action Gate merely to reduce code: those currently protect separate expiry/recovery, freshness, replay, and authorization obligations. Do not change the documented navigation/filter product behavior as an incidental cleanup.
   
   **4. Verify through the real boundaries.**
   
   Use the existing test seams and native E2E infrastructure. Cover ordinary submission and rejected Retry while filtered; ensure read/unknown-admission recovery does not create a duplicate submission. Exercise a docked selector and a hydrated question through the real presentation command handling and assert that placement stays docked. Check progress-card expansion, pending-interaction focus, and floating/docked transitions with the actual window implementation. A no-op Storybook `showConversation` stub cannot establish these contracts.
   
   Complete the PR's already-listed real-model ambiguous-routing → selection → target-execution acceptance, or state precisely why it remains unverified. Investigate the failing exact-head CI `transcript-scroll-cost` E2E without assuming either a PR regression or a flake; preserve any failed aggregate result in the validation report. Run the affected checks rather than adding a broad new test matrix.
   
   **5. Report the final architecture, not just the patch.**
   
   Update the PR description with the problem, the final responsibility boundaries, which old paths or obligations were removed, and the evidence for each acceptance item. Explain any complexity that survived ablation. If a real product decision is needed, surface that specific decision before inventing behavior.
   
   Do not stop after adding `if (progress)` to the current effect. Completion means the contract prevents equivalent mistakes at other callers, the superseded behavior is removed, and the observable interaction/presentation paths have been verified.
   
   ---
   
   I will rereview the consolidated architecture first, then the implementation and simplification. AI assistance: Codex helped trace the current call paths and draft this review guidance.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3990856656


##########
packages/ui/src/choice-panel.tsx:
##########
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef, type ReactNode, type CSSProperties, type KeyboardEvent } from 'react';
+import { useUiLocale } from './locale-context.js';
+import { getConversationCopy } from './conversation-copy.js';
+import { RadioList, RadioListItem } from '@astryxdesign/core';
+
+export interface ChoicePanelOption {
+  readonly value: string;
+  readonly label: string;
+  readonly description?: string;
+  readonly accentColor?: string;
+}
+
+/** Shared single-choice interaction for questions and explicit target selection. */
+export function ChoicePanel(props: {
+  label: string;
+  options: readonly ChoicePanelOption[];
+  value: string;
+  disabled?: boolean;
+  onChange(value: string): void;
+  onConfirm(): void;
+  onEscape(): void;
+  children?: ReactNode;
+}) {
+  const locale = useUiLocale();

Review Comment:
   Locale strings are inlined in the component; the rest of `packages/ui` reads copy through `getConversationCopy(locale)` (see `user-question-prompt.tsx`). Move the hint there so zh-TW/en/zh-CN are maintained in one table.
   文案应进 `getConversationCopy` 表,而不是组件内三元。



##########
apps/desktop/src/renderer/styles/workhub.css:
##########
@@ -299,3 +252,66 @@ body:has(.workHubLive[data-placement='floating'][data-progress='true'])::after {
 @media (prefers-reduced-motion: reduce) {
   .workHubLive[data-placement='floating'], .workHubLive[data-placement='floating']::before, body:has(.workHubLive[data-placement='floating'])::after, .workHubLive[data-conversation-expanded] .workHubHistory { transition: none; }
 }
+
+/* Each message carries Work identity on its sender's edge. */
+.workhub-surface .maka-user-message {
+  border-inline-end: 3px solid transparent;
+  padding-inline-end: 12px;
+}
+
+.workhub-surface .maka-transcript-turn[data-turn-accent='true'] > .maka-turn > .maka-user-message {
+  border-inline-end-color: var(--maka-turn-accent);
+}
+
+.workhub-surface .maka-transcript-turn[data-turn-accent='true'] > .maka-turn > .maka-assistant-answer {
+  border-inline-start: 3px solid var(--maka-turn-accent);
+  padding-inline-start: 12px;
+}
+
+.workhub-turn-heading {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 4px;
+  margin-bottom: 0;
+}
+
+.workhub-turn-label {
+  max-width: 100%;
+  height: auto;
+  min-height: 18px;
+  padding: 0;
+  color: var(--workhub-work-label);
+  font-size: 11px;
+  font-weight: 400;
+  line-height: 16px;
+  white-space: normal;
+  overflow-wrap: anywhere;
+  text-align: right;
+}
+
+.workhub-navigation-workspace {
+  color: var(--workhub-work-label);
+}
+
+.workhub-surface .maka-user-message :has(> .workhub-turn-heading) {
+  row-gap: 4px;
+}
+
+.maka-assistant-answer .workhub-turn-heading {
+  justify-content: flex-start;
+  margin-bottom: -4px;
+}
+
+.maka-assistant-answer .workhub-turn-label {
+  text-align: left;
+}
+
+.workhub-target-selector { width: min(var(--maka-reading-measure), calc(100% - 2 * var(--space-6))); margin-inline: auto; }
+.workhub-selection-hint { margin: 4px 0 0; color: var(--muted-foreground); font-size: 12px; }
+.workhub-target-selector [role="radiogroup"] { max-height: 260px; overflow-y: auto; }

Review Comment:
   `--workhub-selection-label: 0.42 0.075` / `0.8 0.09` duplicates the values of `--workhub-work-label` (lines 102/108). Set `--workhub-work-hue` on each item and reuse `.workhub-work-identity` so the identity colour has one authority.
   与 `--workhub-work-label` 数值重复,复用现有变量即可。



##########
packages/runtime-host/src/server/root-turn-coordinator.ts:
##########
@@ -1565,14 +1566,20 @@ export class RootTurnCoordinator implements HostedExecutionAuthority {
     content: MessageContent,
     execution: RootExecutionDescriptor,
     inputClosedSignal?: AbortSignal,
+    allowTargetSelection = false,

Review Comment:
   `allowTargetSelection` has one caller and it always passes `true` (line 2096); the flag exists only so the coordinator test fixture can toggle it. Drop the parameter or gate on something real.
   `allowTargetSelection` 唯一调用点恒为 true,参数可删。



##########
packages/runtime-host/src/server/workhub-coordination-coordinator.ts:
##########
@@ -77,6 +77,14 @@ const CREATE_FINGERPRINT = `sha256:${createHash('sha256')
 const COORDINATION_CWD_DIRECTORY = 'workhub-coordination';
 const COORDINATION_TOOL_PROFILE = 'workhub-coordination-v2' as const;
 const COORDINATION_PERMISSION_MODE = 'bypass' as const;
+type TargetSelectionRequest =
+  import('../protocol/workhub-coordination.js').WorkHubTargetSelectionRequest;

Review Comment:
   Inline `import('../protocol/workhub-coordination.js')` types here and at lines 810 and 884 — the file already imports from that module at the top; add `WorkHubTargetSelectionRequest` / `WorkHubCoordinationCandidatesResult` to that import.
   类型改为顶部 import,不用内联 `import()`。



##########
apps/desktop/stories/ask-user-question.stories.tsx:
##########
@@ -94,3 +95,19 @@ export const PendingDecisions: Story = {
     onStop: () => {},
   },
 };
+
+export const KeyboardChoices: Story = {
+  ...PendingDecisions,
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.keyboard('2');
+    expect(canvas.getByRole('radio', { name: '公开测试' })).toBeChecked();

Review Comment:
   `expect(...).toBeChecked()` immediately after `userEvent.keyboard('2')` fails in the static build in both colour schemes (React has not committed yet); real key presses do select the radio. Wrap the assertions in `waitFor`, here and in `workhub.stories.tsx:242,252,254,284`.
   断言紧跟按键会在静态构建里失败,包一层 `waitFor`。



##########
apps/desktop/src/renderer/features/workhub/ui/workhub-target-selector.tsx:
##########
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useId, useState } from 'react';
+import { Button } from '@astryxdesign/core';
+import { ChoicePanel, presentSessionStatus, useUiLocale } from '@maka/ui';
+import type { WorkHubTargetSelection, WorkHubTargetSelectionRequest } from '@maka/runtime-host/protocol';
+import { workHubIdentityHue } from './workhub-work-identity.js';
+import { workHubSelectionCopy } from '../locales/workhub-selection-copy.js';
+
+export function WorkHubTargetSelector(props: {
+  request: WorkHubTargetSelectionRequest;
+  submitting: boolean;
+  onChoose(selection: WorkHubTargetSelection): void;
+  onDismiss(): void;
+}) {
+  const locale = useUiLocale();
+  const copy = workHubSelectionCopy[locale];
+  const titleId = useId();
+  const [value, setValue] = useState('');
+  const confirm = () => { if (value && !props.submitting) props.onChoose({ requestId: props.request.requestId, kind: 'existing', candidateRef: value }); };
+  return <section className="maka-composer-interaction workhub-target-selector" aria-labelledby={titleId} aria-busy={props.submitting}>
+    <div className="maka-composer-interaction-inner">
+      <header className="maka-interaction-header">
+        <h2 className="maka-interaction-title" id={titleId}>{copy.title}</h2>
+        <p className="workhub-selection-hint">{props.request.candidates.length ? copy.hint : copy.empty}</p>
+      </header>
+      <ChoicePanel label={copy.title} value={value} onChange={setValue} disabled={props.submitting} onConfirm={confirm} onEscape={props.onDismiss}
+        options={props.request.candidates.map((candidate) => ({
+          value: candidate.candidateRef,
+          label: candidate.sessionName,
+          accentColor: `oklch(var(--workhub-selection-label) ${workHubIdentityHue(candidate.sessionId)})`,
+          description: `${candidate.workspace.hostCwd.replace(/[/\\]+$/, '').split(/[/\\]/).at(-1) ?? ''} · ${presentSessionStatus(candidate.state, locale).label}`,

Review Comment:
   `hostCwd.replace(/[/\\]+$/, '').split(/[/\\]/).at(-1)` is the same expression as `linked-work.ts:64`; one `workspaceNameFromCwd` helper in `model/` keeps Windows/POSIX handling in one place.
   与 linked-work.ts 的路径取名表达式重复,抽一个 helper。



##########
packages/runtime-host/src/server/workhub-coordination-coordinator.ts:
##########
@@ -820,6 +863,83 @@ export class HostWorkHubCoordinationCoordinator {
       // silently become creation or bind an arbitrary existing Session.
       return { kind: 'routing', disposition: 'clarify' };
     }
+    if (
+      input.allowTargetSelection &&
+      decision.kind === 'routing' &&
+      decision.disposition === 'clarify'
+    ) {
+      if (!candidates) {
+        const outcome = await this.#candidates();
+        if (!outcome.ok) return decision;
+        candidates = outcome.result;
+      }
+      this.#requireTargetSelection(input.turnId, contentDigest, candidates);
+    }
+    return decision;
+  }
+
+  #requireTargetSelection(
+    turnId: string,
+    contentDigest: string,
+    candidates: import('../protocol/workhub-coordination.js').WorkHubCoordinationCandidatesResult,
+  ): never {
+    for (const [id, entry] of this.#targetSelections)
+      if (entry.expiresAt <= Date.now()) this.#targetSelections.delete(id);
+    if (this.#targetSelections.size >= 64)
+      this.#targetSelections.delete(this.#targetSelections.keys().next().value!);
+    const request = { ...candidates, requestId: randomUUID() };
+    this.#targetSelections.set(turnId, {
+      digest: contentDigest,
+      request,
+      expiresAt: Date.now() + 10 * 60_000,
+    });
+    throw new TargetSelectionRequired(request);
+  }
+
+  async #selectedRoutingDecision(
+    input: WorkHubCoordinationAnswerInput,
+  ): Promise<WorkHubRoutingDecision> {
+    const contentDigest = digest(
+      normalizeMessageContent({
+        text: input.text,
+        ...(input.attachments ? { attachments: input.attachments } : {}),
+      }),
+    );
+    const pending = this.#targetSelections.get(input.turnId);
+    const current = await this.#candidates();
+    if (!current.ok) throw new Error(current.error.message);
+    if (
+      !pending ||
+      pending.expiresAt <= Date.now() ||
+      pending.request.requestId !== input.selection!.requestId
+    ) {
+      return this.#requireTargetSelection(input.turnId, contentDigest, current.result);
+    }
+    if (pending.digest !== contentDigest)
+      throw new Error('WorkHub selection belongs to a different request');

Review Comment:
   Plain `throw new Error(...)` here and at 923/927 reaches the dispatcher as `internal_failure` with a console error for what is a client mismatch. Return `{ kind: 'rejected', outcome: turnFailure('operation_conflict', …) }` through `prepareFreshContent` instead (this also disappears with the P0 fix, which must stop throwing through `startRootMessage`).
   客户端不匹配不应以异常穿透到 dispatcher,改为 operation_conflict outcome。



##########
apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx:
##########
@@ -80,6 +81,12 @@ export function WorkHubRoot() {
   const surface = useRef<HTMLElement>(null);
   const [editingProgressRequest, setEditingProgressRequest] = useState<number>();
   const [expandedOverride, setConversationExpanded] = useState<boolean>();
+  const promptStates = new Map<string, import('../model/linked-work.js').WorkHubDelegationState>();
+  for (const message of transcript.messages) if (message.type === 'turn_state') promptStates.set(message.turnId, message.status);

Review Comment:
   Every historical `turn_state` gets a status, so completed prompts render "Completed · time" forever (verified in `product-workhub--question-lifecycle`). A terminal `completed` on a plain coordination turn changes nothing the user does next; keep `running` / `waiting_for_user` / `failed` / `aborted` and delegation states only.
   每条已完成 prompt 都显示"已完成",是噪音;只保留非终态和失败/中止。



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3996524052


##########
apps/desktop/src/main/workhub-presentation.ts:
##########
@@ -535,29 +535,25 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
             return;
           }
           case 'show-conversation': {
-            if (payload !== undefined && payload !== progressRequest) return;
+            if (payload !== progressRequest || !floating) return;
             conversationExpanded = true;
             expandOnFocus = true;
-            if (progressRequest !== undefined && floating) {
-              const current = floating.getBounds();
-              const area = screen.getDisplayMatching(current).workArea;
-              const width = Math.min(conversationBounds?.width ?? 520, area.width);
-              const height = Math.min(expandedHeight, area.height);
-              clearProgressRequest();
-              conversationBounds = undefined;
-              floating.setResizable(true);
-              changed();
-              resizeFloating({
-                width, height,
-                x: Math.max(area.x, Math.min(current.x + Math.round((current.width - width) / 2), area.x + area.width - width)),
-                y: Math.max(area.y, Math.min(current.y + current.height - height, area.y + area.height - height)),
-              }, true);
-              // A send acknowledgement can arrive after the user has switched
-              // apps. Growing the conversation must not steal focus back.
-              if (floating.isFocused()) focusComposer();
-              return;
-            }
-            detach(true);
+            const current = floating.getBounds();
+            const area = screen.getDisplayMatching(current).workArea;
+            const width = Math.min(conversationBounds?.width ?? 520, area.width);
+            const height = Math.min(expandedHeight, area.height);
+            clearProgressRequest();

Review Comment:
   **[P2] Finish showing the native window when expansion beats progress paint.** Reachability: ordinary interaction delivery or hydration while a progress window is being prepared. `requestProgress()` initially keeps the floating window hidden until `progress-ready`; that acknowledgement is delayed by two animation frames in `WorkHubProgressCard`. The updated Root effect can call `expandProgress(request)` first when a question/selector is already pending.
   
   This branch clears the request and resizes the still-hidden window, but never shows it. Root then unmounts the progress card, cancelling its paint callback; even a late `progress-ready(request)` is ignored because the request was cleared. I reproduced this through the existing main-handler harness: `prepareControl('turn') → ready → show-conversation(request) → progress-ready(request)` ends with `placement: 'floating'`, `floatingVisible: false`, and no progress request. The waiting interaction stays invisible until a manual summon.
   
   Please make a valid current progress expansion complete the native reveal in this same owner, using the existing reveal-mode/no-focus-stealing policy. Keep stale/dismissed requests inert and do not restore the no-argument detach fallback. Add the before-first-paint ordering to the existing presentation regression.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on PR #5198:
URL: https://github.com/apache/maka/pull/5198#issuecomment-5646728869

   Thanks for working through all the follow-ups! It would be helpful to add a couple of short demo videos to the PR body, both to show how the feature feels and to make the remaining acceptance easier to verify:
   
   - The actual progress window automatically expanding into a question or target selector, then completing the interaction without stealing focus.
   - A real-model ambiguous request going through target selection and executing against the selected target.
   
   A brief note with the tested commit, OS, steps, and whether the backend is real or simulated would be enough. No polished walkthrough needed—a simple screen recording of each flow would help me review the behavior and give future readers a useful feature demo. Thanks!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "ARE404 (via GitHub)" <gi...@apache.org>.
ARE404 commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3996586484


##########
apps/desktop/src/main/workhub-presentation.ts:
##########
@@ -535,29 +535,25 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
             return;
           }
           case 'show-conversation': {
-            if (payload !== undefined && payload !== progressRequest) return;
+            if (payload !== progressRequest || !floating) return;
             conversationExpanded = true;
             expandOnFocus = true;
-            if (progressRequest !== undefined && floating) {
-              const current = floating.getBounds();
-              const area = screen.getDisplayMatching(current).workArea;
-              const width = Math.min(conversationBounds?.width ?? 520, area.width);
-              const height = Math.min(expandedHeight, area.height);
-              clearProgressRequest();
-              conversationBounds = undefined;
-              floating.setResizable(true);
-              changed();
-              resizeFloating({
-                width, height,
-                x: Math.max(area.x, Math.min(current.x + Math.round((current.width - width) / 2), area.x + area.width - width)),
-                y: Math.max(area.y, Math.min(current.y + current.height - height, area.y + area.height - height)),
-              }, true);
-              // A send acknowledgement can arrive after the user has switched
-              // apps. Growing the conversation must not steal focus back.
-              if (floating.isFocused()) focusComposer();
-              return;
-            }
-            detach(true);
+            const current = floating.getBounds();
+            const area = screen.getDisplayMatching(current).workArea;
+            const width = Math.min(conversationBounds?.width ?? 520, area.width);
+            const height = Math.min(expandedHeight, area.height);
+            clearProgressRequest();

Review Comment:
   Confirmed and fixed in 154f4a33d. I reproduced the exact pre-paint ordering through the existing main-handler harness: prepareControl → ready → expand(request) left floatingVisible false. The regression was red before the fix and green afterward.
   
   The existing presentation owner now calls showWindowInactive after restoring conversation geometry. This completes the reveal when expansion cancels the progress paint callback, honors hidden reveal mode, and does not activate the window or move focus into the composer when the window is inactive. The current/stale request checks and explicit detach/dock paths are unchanged. The regression also checks that late progress-ready and stale expansion cannot reopen the window after dismissal.
   
   Rebased onto main 5049ffc1a, retaining both main’s transcript commit-scheduler regression and this PR’s failure/retry behavior. Validation: full build and workspace typecheck; 2532 Desktop tests; 48 focused presentation/controller tests; 112 architecture tests; five native Electron cases including Host history paging, window reconstruction, docked question hydration and explicit detach/dock with retained selection/focus. Lint, format, ASF, inventory and E2E budget checks pass.
   
   The prior head’s CI passed; the rebased head has a fresh run pending. Real-provider routing and the complete native automatic-progress/selector flow remain explicitly unverified in the PR description. This main-handler regression does not claim to close those acceptance items.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): add work identity, prompt status, and keyboard selection [maka]

Posted by "ARE404 (via GitHub)" <gi...@apache.org>.
ARE404 commented on PR #5198:
URL: https://github.com/apache/maka/pull/5198#issuecomment-5646794637

   Ran the production Electron build at 59565b1aa with real deepseek-v4-flash, isolated data, and no injected model/presentation responses.
   
   The native progress → automatic question expansion → keyboard answer → completion path passed. The progress window became visible without taking focus from the main window; after explicit activation, 2 + Enter selected blue and the real model completed with “你选择了蓝色。”
   
   The real model also resolved two same-named works through a generic AskUserQuestion, then delegated to the chosen beta Session. beta/acceptance.txt exists with exactly the requested 32 bytes; alpha has no file. I verified the target Session's execution and completion, not merely admission.
   
   However, the dedicated target selector did not appear. Source tracing explains why: execution-composition only installs prepareRoutingDecision when dependencies.workHubRoutingModel is provided (lines 1525–1527), and passes that same optional dependency to the coordinator (line 1942). The current production entry points do not supply it; the factory and injection are used only by tests. These lines came from #5152, before this PR. The generic question flow therefore cannot close the dedicated-selector acceptance. This is now recorded as a production integration gap rather than unexecuted testing.
   
   Reproducible prompts, actual transition timestamps, focus observations, Session identities, file evidence and screenshots: https://github.com/ARE404/maka-agent/blob/6aa526a8d6c7e45b2ec656152ee02120339322ca/docs/workhub-native-acceptance-2026-09-12.zh-CN.md
   
   The tested head's CI also failed separately in colored-work-history (expected 已完成, observed 已接收); that failure remains recorded. This evidence-only commit does not change production routing or fix that story.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


Re: [PR] feat(workhub): unify work identity and Host-owned target selection [maka]

Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #5198:
URL: https://github.com/apache/maka/pull/5198#discussion_r3999052740


##########
packages/ui/src/form-interaction-prompt.tsx:
##########
@@ -141,23 +160,26 @@ function ActiveFormInteractionPrompt(props: {
 
         <footer className="maka-interaction-actions maka-form-interaction-actions">
           <div>
+            {props.onStop && <Button variant="ghost" isDisabled={props.stopPending}
+              onClick={() => void props.onStop?.()}
+              label={props.stopPending ? conversationCopy.questions.stopping : conversationCopy.questions.stop} />}
             <Button
               variant="ghost"
-              isDisabled={responsePending}
+              isDisabled={responsePending || props.stopPending}
               onClick={() => void respond({ requestId: props.request.requestId, action: 'cancel' })}
               label={copy.cancel}
             />
           </div>
           <div className="maka-form-interaction-primary-actions">
-            <Button
+            {!singleChoice && <Button

Review Comment:
   [P2] Preserve Decline for shared single-select Forms
   
   Could this shared Form keep its Decline action in singleChoice mode? The condition applies to any required, single-field single-select Form, including ordinary MCP forms, rather than only WorkHub target confirmation. The existing protocol distinguishes `decline` from `cancel` and passes that distinction back to the requesting tool. Rendering such a Form at this head confirms that only Cancel and Submit remain, so the user can no longer explicitly decline. Keeping the existing button is enough; WorkHub already treats either non-accept result as no delegation.



##########
apps/desktop/src/renderer/features/workhub/ui/workhub-conversation.tsx:
##########
@@ -44,53 +42,110 @@ export function WorkHubResultCard(props: {
     aborted: copy.delegationAborted,
     recovering: copy.delegationRecovering,
   }[state];
-  return <div className="workhub-message-identity workhub-work-identity"
-    style={{ '--workhub-work-hue': workHubIdentityHue(work.targetSessionId) } as CSSProperties}
-    data-work-session-id={work.targetSessionId}
-    data-work-highlighted={props.highlighted}
-    data-work-state={state}
-    onMouseEnter={() => props.onHighlight(true)}
-    onMouseLeave={() => props.onHighlight(false)}
-    onFocus={() => props.onHighlight(true)}
-    onBlur={() => props.onHighlight(false)}>
-    <div className="workhub-result-card">
-      <div className="workhub-result-heading">
-        <strong>{work.targetSessionName}</strong>
-        <span role="status">{stateLabel}</span>
-      </div>
-      {work.resultPreview ? <p>{work.resultPreview}</p> : null}
-      <Button
-        variant="ghost"
-        label={work.resultPreview ? copy.openResult : copy.openWork}
-        onClick={() => props.onOpenWork(work.targetSessionId)}
-      />
-    </div>
-  </div>;
+  return <Text type="supporting" color="secondary" className="workhub-delegation-status" role="status" data-work-state={state}>
+    {props.showName ? `${work.targetSessionName}: ` : ''}{stateLabel}
+  </Text>;
 }
 
-export function WorkHubConversation(props: ComponentProps<typeof ChatView> & { workLinks: readonly WorkHubLinkedWork[]; onOpenWork(sessionId: string): void }) {
-  const { onOpenWork, workLinks: assignments, ...chat } = props;
+export function WorkHubConversation(props: ComponentProps<typeof ChatView> & { workLinks: readonly WorkHubLinkedWork[]; onOpenWork(sessionId: string): void; promptStates?: ReadonlyMap<string, WorkHubDelegationState> }) {
+  const { onOpenWork, workLinks: assignments, promptStates, ...chat } = props;
   const highlight = useContext(WorkHubHighlightContext);
   const locale = useUiLocale();
-  const workByTurn = useMemo(() => new Map(assignments.map((assignment) => [assignment.coordinationTurnId, assignment.targetSessionId])), [assignments]);
+  const copy = workHubLiveCopy[locale];
+  const [loadingHistory, setLoadingHistory] = useState(false);
+  const [historyError, setHistoryError] = useState(false);
+  async function loadHistory(edge: 'older' | 'newer') {
+    if (loadingHistory) return;
+    setLoadingHistory(true);
+    setHistoryError(false);
+    try { await chat.onPrefetchHistory?.(edge); }
+    catch { setHistoryError(true); }
+    finally { setLoadingHistory(false); }
+  }
+  // A coordination turn can delegate to several Works. Keep every label and
+  // leave its shared bar neutral rather than attributing the entire turn to one.
+  const worksByTurn = useMemo(() => {
+    const grouped = new Map<string, WorkHubLinkedWork[]>();
+    for (const work of assignments) {
+      const works = grouped.get(work.coordinationTurnId) ?? [];
+      if (!works.some((item) => item.targetSessionId === work.targetSessionId)) works.push(work);

Review Comment:
   [P2] Keep delegation statuses separate when deduplicating Session identity
   
   Could the Session-label deduplication leave the individual delegation records available for status rendering? Different tool calls in one Coordination Turn can delegate to the same Session under different action ids, and feedback is keyed by assignment id. Keeping only the first link here hides later work: a rendered example with the first assignment completed and the second running produces one completed status and no running status. The Session label/accent can still be shared, while the existing per-assignment feedback remains represented. This is a projection fix; no Host or storage redesign is needed.



##########
packages/runtime-host/src/server/workhub-coordination-coordinator.ts:
##########
@@ -536,9 +548,172 @@ export class HostWorkHubCoordinationCoordinator {
     }
   }
 
+  async #selectAndDelegate(
+    input: WorkHubCoordinationSelectAndDelegateInput,
+    context: ConnectionContext,
+  ): Promise<OperationOutcome<'workhub.coordination.selectAndDelegate'>> {
+    try {
+      const authority = await this.#executions.readActiveWorkHubRoutingRequest(input.turnId);
+      if (!authority || authority.decision || !this.#requestForm) {
+        return {
+          ok: false,
+          error: {
+            code: 'operation_conflict',
+            message: 'Target selection requires an active, unbound Coordination Turn',
+          },
+        };
+      }
+      // The interaction identity binds the complete operation and original user authority.
+      // Its durable answer can be replayed without rebuilding (or interpreting) the offer.
+      const requestId = createHash('sha256')
+        .update(
+          JSON.stringify({
+            input,
+            runId: authority.runId,
+            content: authority.content,
+          }),
+        )
+        .digest('hex');
+      const choice = await this.#requestForm({
+        sessionId: WORKHUB_COORDINATION_SESSION_ID,
+        turnId: input.turnId,
+        runId: authority.runId,
+        requestId,
+        create: async () => {
+          if (await this.#stores.readWorkHubAssignment(input.actionId)) {
+            throw new WorkHubActionGateFailure(
+              'action_conflict',
+              'This action identity already belongs to another selection',
+            );
+          }
+          const page = await this.#actionGate.candidates();
+          if (page.candidateSetId !== input.candidateSetId) {
+            throw new WorkHubActionGateFailure(
+              'candidate_set_stale',
+              'Discover fresh candidates before requesting a target choice',
+            );
+          }
+          const options = input.candidateRefs.map((ref) => {
+            const candidate = page.candidates.find((item) => item.candidateRef === ref);
+            if (!candidate)
+              throw new WorkHubActionGateFailure(
+                'candidate_unavailable',
+                'Target choice contains an unavailable candidate',
+              );
+            // Keep labels inside the shared form wire budget; identity is the opaque value.
+            const label = `${candidate.sessionName} — ${candidate.workspace.hostCwd}`;
+            let bounded = '';
+            for (const character of label) {
+              if (Buffer.byteLength(bounded + character, 'utf8') > 190) break;
+              bounded += character;
+            }
+            return {
+              // An opaque, durable binding: only an exact offered value is accepted by
+              // the form authority. Names and model-written answers never resolve identity.
+              value: JSON.stringify([ref, candidate.sessionId, digest(candidate.workspace)]),
+              label: page.candidates.some(

Review Comment:
   [P2] Keep option labels distinguishable after truncation
   
   Could the uniqueness check run on the final bounded labels? Two Sessions with the same long name in different workspaces can lose the distinguishing path suffix during the 190-byte truncation. Since the full cwd values differ, neither receives the short Session id here. The shared Form then rejects the request with `Duplicate form option label`, surfaced as `persistence_failed`, before any selector appears. I reproduced this through the production composition handler with two valid 80-character Chinese names and different directories. Reserving space for a stable distinguishing suffix when the bounded labels collide should fix this locally; a focused regression through Form publication would protect the behavior.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org