Skip to content

Commit d433b5b

Browse files
committed
feat(messages): navigable message actions that focus a dock
Add an optional `actions` field to `DevframeMessageEntry` (a discriminated union, `activate` implemented) so a message can request the viewer switch its focused dock, deep-linking via the existing `hub:docks:activate` primitive. The messages panel renders the actions in the detail pane and only when a hub docks-activate capability is present, so standalone panels show no dead buttons. Co-authored-with an agent.
1 parent 47756fa commit d433b5b

7 files changed

Lines changed: 79 additions & 3 deletions

File tree

‎packages/hub/src/types/messages.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ export interface DevframeMessageFilePosition {
2121
column?: number
2222
}
2323

24+
/**
25+
* A labeled control a message can carry. Rendered by the messages panel; when
26+
* clicked it drives the described intent. Discriminated by `kind` so further
27+
* action kinds can be added without reshaping the field.
28+
*
29+
* `'activate'` requests the viewer switch its focused dock to `activate.dockId`
30+
* (deep-linking via the opaque, serializable `activate.params` bag the target
31+
* dock interprets), via the hub's `hub:docks:activate` RPC.
32+
*/
33+
export interface DevframeMessageActivateAction {
34+
/** Stable id for the action within its entry. */
35+
id: string
36+
/** Button label shown in the messages panel. */
37+
label: string
38+
kind: 'activate'
39+
/** The dock to focus, plus an optional deep-link params bag. */
40+
activate: { dockId: string, params?: Record<string, unknown> }
41+
}
42+
43+
export type DevframeMessageAction = DevframeMessageActivateAction
44+
2445
export interface DevframeMessageEntry {
2546
/**
2647
* Unique identifier for this message entry (auto-generated if not provided)
@@ -66,6 +87,11 @@ export interface DevframeMessageEntry {
6687
* Optional tags/labels for filtering
6788
*/
6889
labels?: string[]
90+
/**
91+
* Optional labeled actions (e.g. "navigate to a dock") the panel renders as
92+
* clickable controls in the entry's detail view.
93+
*/
94+
actions?: DevframeMessageAction[]
6995
/**
7096
* Time in ms to auto-dismiss the toast notification (client-side)
7197
*/

‎plugins/messages/src/client/App.vue‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
22
import type { DevframeConnectionStatus, DevframeRpcClient } from 'devframe/client'
3-
import type { DevframeMessageEntry } from '../types'
3+
import type { DevframeMessageAction, DevframeMessageEntry } from '../types'
44
import LayoutToolbar from '@antfu/design/components/Layout/LayoutToolbar.vue'
5-
import { computed, onBeforeUnmount, ref } from 'vue'
5+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
66
import {
77
button,
88
connectionBody,
@@ -46,6 +46,29 @@ function reload(): void {
4646
// an editor with.
4747
const canOpenFile = computed(() => props.rpc.connectionMeta.backend !== 'static')
4848
49+
// Message actions that navigate to another dock only work under a hub host
50+
// (the `hub:docks:activate` RPC + `devframe:docks` registry). Probe the docks
51+
// shared state so the affordance is hidden when there's no hub.
52+
const canActivate = ref(false)
53+
onMounted(() => {
54+
props.rpc.sharedState
55+
.get('devframe:docks', { initialValue: null })
56+
.then((state: { value: () => unknown, on: (e: string, cb: (v: unknown) => void) => void }) => {
57+
canActivate.value = state.value() != null
58+
state.on('updated', v => (canActivate.value = v != null))
59+
})
60+
.catch(() => {})
61+
})
62+
63+
async function onActivate(action: DevframeMessageAction): Promise<void> {
64+
if (action.kind !== 'activate')
65+
return
66+
await (props.rpc.callOptional as (name: string, ...args: unknown[]) => Promise<unknown>)(
67+
'hub:docks:activate',
68+
action.activate,
69+
)
70+
}
71+
4972
async function onDismiss(id: string): Promise<void> {
5073
await props.rpc.call('devframes:plugin:messages:remove', id)
5174
}
@@ -124,11 +147,13 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
124147
v-else
125148
:entries="state.entries"
126149
:can-open-file="canOpenFile"
150+
:can-activate="canActivate"
127151
@dismiss="onDismiss"
128152
@dismiss-many="onDismissMany"
129153
@clear="onClear"
130154
@persist="onPersist"
131155
@open-file="onOpenFile"
156+
@activate="onActivate"
132157
/>
133158
</div>
134159
</div>

‎plugins/messages/src/client/components/MessagesView.vue‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import type { DevframeMessageEntry, DevframeMessageEntryFrom, DevframeMessageLevel } from '../../types'
2+
import type { DevframeMessageAction, DevframeMessageEntry, DevframeMessageEntryFrom, DevframeMessageLevel } from '../../types'
33
import { useClipboard, useTimeAgo } from '@vueuse/core'
44
import { computed, ref, watch } from 'vue'
55
import FilterToggles from './FilterToggles.vue'
@@ -20,6 +20,8 @@ const props = defineProps<{
2020
entries: DevframeMessageEntry[]
2121
/** Show the "open file" affordance for entries with a `filePosition`. */
2222
canOpenFile?: boolean
23+
/** Show entry `actions` (dock navigation works only under a hub host). */
24+
canActivate?: boolean
2325
}>()
2426
2527
const emit = defineEmits<{
@@ -33,6 +35,8 @@ const emit = defineEmits<{
3335
openFile: [entry: DevframeMessageEntry]
3436
/** Cancel an entry's `autoDelete` timer (viewing it pins it) */
3537
persist: [id: string]
38+
/** Run one of the entry's actions (e.g. navigate to a dock) */
39+
activate: [action: DevframeMessageAction]
3640
}>()
3741
3842
function formatAbsoluteTime(ts: number): string {
@@ -387,6 +391,19 @@ function dismissFiltered() {
387391
<HashBadge v-for="label of selectedEntry.labels" :key="label" :label="label" class="cursor-pointer" @click="toggleLabelFilter(label)" />
388392
</div>
389393

394+
<!-- Actions (e.g. navigate to a dock) -->
395+
<div v-if="canActivate && selectedEntry.actions && selectedEntry.actions.length" class="flex flex-wrap gap-1.5 mb-3">
396+
<button
397+
v-for="action of selectedEntry.actions"
398+
:key="action.id"
399+
class="flex items-center gap-1.5 text-sm px-2 py-1 rounded border border-base hover:bg-active transition"
400+
@click="emit('activate', action)"
401+
>
402+
<div class="i-ph:arrow-square-out-duotone w-3.5 h-3.5" />
403+
{{ action.label }}
404+
</button>
405+
</div>
406+
390407
<!-- File position -->
391408
<button
392409
v-if="selectedEntry.filePosition && canOpenFile"

‎plugins/messages/src/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// The plugin's data vocabulary is the hub's — re-exported so the SPA, the
22
// embeddable client, and consumers can type against the plugin package alone.
33
export type {
4+
DevframeMessageAction,
5+
DevframeMessageActivateAction,
46
DevframeMessageEntry,
57
DevframeMessageEntryFrom,
68
DevframeMessageEntryInput,

‎tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export { DevframeDocksUserSettings }
5050
export { DevframeDockUserEntry }
5151
export { DevframeHost }
5252
export { DevframeHubContext }
53+
export { DevframeMessageAction }
54+
export { DevframeMessageActivateAction }
5355
export { DevframeMessageElementPosition }
5456
export { DevframeMessageEntry }
5557
export { DevframeMessageEntryFrom }

‎tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export { DevframeDocksUserSettings }
3434
export { DevframeDockUserEntry }
3535
export { DevframeHost }
3636
export { DevframeHubContext }
37+
export { DevframeMessageAction }
38+
export { DevframeMessageActivateAction }
3739
export { DevframeMessageElementPosition }
3840
export { DevframeMessageEntry }
3941
export { DevframeMessageEntryFrom }

‎tests/__snapshots__/tsnapi/@devframes/plugin-messages/types.snapshot.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Generated by tsnapi — public API snapshot of `@devframes/plugin-messages/types`
33
*/
44
// #region Other
5+
export { DevframeMessageAction }
6+
export { DevframeMessageActivateAction }
57
export { DevframeMessageEntry }
68
export { DevframeMessageEntryFrom }
79
export { DevframeMessageEntryInput }

0 commit comments

Comments
 (0)